I'm trying to play with SwiftUI and make a map with dropped pins from locations (generated from a database API).
I have my struct:
struct Locations: Decodable, Identifiable {
var id: Int { _id }
let _id: Int // the one used in the database
let streetaddress: String?
let suburb: String?
let state: String?
let postcode: String?
// get the co-ordinates now
var coordinates: CLLocationCoordinate2D? {
let geocoder = CLGeocoder()
var output = CLLocationCoordinate2D()
if let address = streetaddress,
let suburb = suburb,
let postcode = postcode,
let state = state {
let fullAddress = "\(address) \(suburb), \(state) \(postcode)"
geocoder.geocodeAddressString( String(fullAddress) ) { ( placemark, error ) in
if let latitude = placemark?.first?.location?.coordinate.latitude,
let longitude = placemark?.first?.location?.coordinate.longitude {
output = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
}
}
return output
}
}
However, whenever I call the coordinates I am getting a {"msg":"#NullIsland Received a latitude or longitude from getLocationForBundleID that was exactly zero", "latIsZero":0, "lonIsZero":0} error.
I have added the error snippet from here: https://stackoverflow.com/a/65837163/1086990 to dive deeper into the error, and it is returning network: network was unavailable or a network error occurred.
I am able to call Map() in the view, and permissions are set in the info.plist as well as see my current location, etc.
Is there something I'm missing or is it not calculating because the Strings are all optional? Been trying to understand how it's not generating the coordinates from the address.
If I try to debug in the view I tried this:
struct MapView: View {
var body: some View {
Text("Hello World")
.onAppear {
for l in modelData.locations {
print( String("\(l.coordinates)") )
}
}
}
}
// console
// Optional(__C.CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0))
// ...
// Optional(__C.CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0))
The issue there is that geocodeAddressString is an asynchronous method. You are returning the value before receiving the result. What you need is a method instead of a computed property and a completion handler.
func coordinate(completion: #escaping (CLLocationCoordinate2D?, Error?) -> Void) {
let streetAddress = streetAddress ?? ""
let suburb = suburb ?? ""
let postCode = postCode ?? ""
let state = state ?? ""
let fullAddress = "\(streetAddress) \(suburb), \(state) \(postCode)"
CLGeocoder().geocodeAddressString(fullAddress) { completion($0?.first?.location?.coordinate, $1) }
}
Usage:
#State var location = Location(id: 1, streetAddress: "One Infinite Loop", suburb: "Cupertino", state: "CA", postCode: "95014")
var body: some View {
Text("Hello, world!")
.padding()
.onAppear {
location.coordinate { coordinate, error in
guard let coordinate = coordinate else {
print("error:", error ?? "nil")
return
}
print("coordinate", coordinate)
}
}
}
This will print
coordinate CLLocationCoordinate2D(latitude: 37.331656, longitude: -122.0301426)
as mentioned geocodeAddressString is an asynchronous function. That means you have to use some completion handler. So you cannot use it like you do with coordinates.
Here is some very basic code using a function instead:
struct Locations: Decodable, Identifiable {
var id: String = UUID().uuidString
// let _id: Int // the one used in the database
let streetaddress: String? = "1 Infinite Loop"
let suburb: String? = "Cupertino"
let state: String? = "CA"
let postcode: String? = ""
func getCoordinates(handler: #escaping ((CLLocationCoordinate2D) -> Void)) {
if let address = streetaddress, let suburb = suburb, let postcode = postcode, let state = state {
let fullAddress = "\(address) \(suburb), \(state) \(postcode)"
CLGeocoder().geocodeAddressString(fullAddress) { ( placemark, error ) in
handler(placemark?.first?.location?.coordinate ?? CLLocationCoordinate2D())
}
}
}
}
struct ContentView: View {
let locs = Locations()
#State var coordString = ""
var body: some View {
Text(locs.streetaddress ?? "no address")
Text(coordString)
.onAppear {
locs.getCoordinates() { coords in
coordString = "\(coords.latitude), \(coords.longitude)"
}
}
}
}
Related
I am having trouble with working over data I receive from server.
Each time server sends data it is new coordinates for each user. I am looping over each incoming data, and I want to send the data in completion to receive them on other end. And update model class with them. At the moment I have two users in server. And sometimes the closure passes data two times, but sometimes just one. And interesting thing is that class properties are not updated, at least I dont see them on UI.
This is function I call when data is received. Response is just string I split to get user data.
func updateUserAdrress(response: String, completion: #escaping (Int, Double, Double, String) -> Void){
var data = response.components(separatedBy: "\n")
data.removeLast()
data.forEach { part in
let components = part.components(separatedBy: ",")
let userID = components[0].components(separatedBy: " ")
let id = Int(userID[1])
let latitude = Double(components[1])!
let longitude = Double(components[2])!
let location = CLLocation(latitude: latitude, longitude: longitude)
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
if error == nil {
let placemark = placemarks?[0]
if let thoroughfare = placemark?.thoroughfare, let subThoroughfare = placemark?.subThoroughfare {
let collectedAddress = thoroughfare + " " + subThoroughfare
DispatchQueue.main.async {
completion(id!, latitude, longitude, collectedAddress)
}
}
} else {
print("Could not get address \(error!.localizedDescription)")
}
}
}
}
In this function I try to invoke the changes on objects. As the incoming data from server is different the first time I have splited the functionality, so the correct block of code would be called.
func collectUsers(_ response: String){
if users.count != 0{
updateUserAdrress(response: response) { id, latitude, longitude, address in
if let index = self.users.firstIndex(where: { $0.id == id }){
let user = self.users[index]
user.latitude = latitude
user.longitude = longitude
user.address = address
}
}
}else{
var userData = response.components(separatedBy: ";")
userData.removeLast()
let users = userData.compactMap { userString -> User? in
let userProperties = userString.components(separatedBy: ",")
var idPart = userProperties[0].components(separatedBy: " ")
if idPart.count == 2{
idPart.removeFirst()
}
guard userProperties.count == 5 else { return nil }
guard let id = Int(idPart[0]),
let latitude = Double(userProperties[3]),
let longitude = Double(userProperties[4]) else { return nil }
let collectedUser = User(id: id, name: userProperties[1], image: userProperties[2], latitude: latitude, longitude: longitude)
return collectedUser
}
DispatchQueue.main.async {
self.users = users
}
}
}
As I also need user address when app starts in model I have made simular function to call in init so it would get address for user. That seems to be working fine. But for more context I will add the model to.
class User: Identifiable {
var id: Int
let name: String
let image: String
var latitude: Double
var longitude: Double
var address: String = ""
init(id: Int, name: String, image: String, latitude: Double, longitude: Double){
self.id = id
self.name = name
self.image = image
self.latitude = latitude
self.longitude = longitude
getAddress(latitude: latitude, longitude: longitude) { address in
self.address = address
}
}
func getAddress(latitude: Double, longitude: Double, completion: #escaping (String) -> Void){
let location = CLLocation(latitude: latitude, longitude: longitude)
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
if error == nil {
let placemark = placemarks?[0]
if let thoroughfare = placemark?.thoroughfare, let subThoroughfare = placemark?.subThoroughfare {
let collectedAddress = thoroughfare + " " + subThoroughfare
completion(collectedAddress)
}
} else {
print("Could not get address \(error!.localizedDescription)")
}
}
}
}
And one interesting thing. That when closure receives two times data and I assign them to class, there are no changes on UI.
I made this project on Xcode 13.4.1 because on Xcode 14 there is a bug on MapAnnotations throwing purple warnings on view changes.
I am trying to build a small Map app where location for user changes all the time. In general I get latitude and longitude updates all the time. And I need to display them and show the change with sliding animation, simular to Apple FindMyFriend, when it slides over map when they are moving in live.
This is my view:
struct ContentView: View {
#StateObject var request = Calls()
#State private var mapRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 56.946285, longitude: 24.105078), span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02))
var body: some View {
Map(coordinateRegion: $mapRegion, annotationItems: $request.users){ $user in
withAnimation(.linear(duration: 2.0)) {
MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: user.latitude, longitude: user.longitude)){
Circle()
}
}
}
}
}
And function call in view model, whitch changes user location, the response is just incoming string from API:
func collectUsers(_ response: String){
if users.count != 0{
var data = response.components(separatedBy: "\n")
data.removeLast()
let updates = self.users.map{ user -> User in
let newData = updateUserLocation(user: user, input: data)
return User(id: user.id, name: user.name, image: user.image, latitude: Double(newData[1])!, longitude: Double(newData[2])!)
}
DispatchQueue.main.async {
self.users = updates
}
}else{
var userData = response.components(separatedBy: ";")
userData.removeLast()
let users = userData.compactMap { userString -> User? in
let userProperties = userString.components(separatedBy: ",")
var idPart = userProperties[0].components(separatedBy: " ")
if idPart.count == 2{
idPart.removeFirst()
}
guard userProperties.count == 5 else { return nil }
guard let id = Int(idPart[0]),
let latitude = Double(userProperties[3]),
let longitude = Double(userProperties[4]) else { return nil }
return User(id: id, name: userProperties[1], image: userProperties[2], latitude: latitude, longitude: longitude)
}
DispatchQueue.main.async {
self.users = users
}
}
}
And ofcourse my #Published:
class Calls: ObservableObject{
#Published var users = [User]()
When I use the MapMarker instead of MapAnnotation the error does not appier. I would use marker, but I need each user view in map to be different.
If any one stumbles with the same issue. I spent entire day to solve this, but the awnser is that in Xcode 14 it is a bug. After I installer Xcode 13.4.1 error messages disappiered.
I have a function that takes my coordnaties that are stored inside of my Firebase Storage, and turns them into MKPointAnnotations For Some Reason I keep on getting the error Type '()' cannot conform to 'View'
Here is my code for the function:
import SwiftUI
import MapKit
import CoreLocationUI
import Firebase
import FirebaseFirestore
struct Marker: Identifiable {
let id = UUID()
var coordinate : CLLocationCoordinate2D
}
struct MapView: View {
#StateObject private var viewModel = ContentViewModel()
//For GeoCoder
let geocoder = CLGeocoder()
#State private var result = "result of lat & long"
#State private var lat = 0.0
#State private var long = 0.0
#State private var country = "country name"
#State private var state = "state name"
#State private var zip = "zip code"
//For Map Annotations
#State var address = ""
#State var realLat = 0.00
#State var realLong = 0.00
#State var email = ""
//For TopBar
#State var goToAddress = ""
#State var filters = false
var body: some View {
let markers = [
Marker(coordinate: CLLocationCoordinate2D(latitude: realLat, longitude: realLong))
]
NavigationView {
VStack {
ZStack (alignment: .bottom) {
LocationButton(.currentLocation) {
viewModel.requestAllowOnceLocationPermission()
}
.foregroundColor(.white)
.cornerRadius(8)
.labelStyle(.iconOnly)
.symbolVariant(.fill)
.tint(.pink)
.padding(.bottom)
.padding(.trailing, 300)
getAnnotations { (annotations) in
if let annotations = annotations {
Map(coordinateRegion: $viewModel.region, showsUserLocation: true, annotationItems: MKPointAnnotation) { annotations in
MapAnnotation(coordinate: annotations.coordinate) {
Circle()
}
}
.ignoresSafeArea()
.tint(.pink)
} else {
print("There has been an error with the annotations")
}
}
}
}
}
}
func getAnnotations(completion: #escaping (_ annotations: [MKPointAnnotation]?) -> Void) {
let db = Firestore.firestore()
db.collection("annotations").addSnapshotListener { (querySnapshot, err) in
guard let snapshot = querySnapshot else {
if let err = err {
print(err)
}
completion(nil) // return nil if error
return
}
guard !snapshot.isEmpty else {
completion([]) // return empty if no documents
return
}
var annotations = [MKPointAnnotation]()
for doc in snapshot.documents {
if let lat = doc.get("lat") as? String,
let lon = doc.get("long") as? String,
let latitude = Double(lat),
let longitude = Double(lon) {
let coord = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
let annotation = MKPointAnnotation()
annotation.coordinate = coord
annotations.append(annotation)
}
}
completion(annotations) // return array
}
}
func goToTypedAddress() {
geocoder.geocodeAddressString(goToAddress, completionHandler: {(placemarks, error) -> Void in
if((error) != nil){
print("Error", error ?? "")
}
if let placemark = placemarks?.first {
let coordinates:CLLocationCoordinate2D = placemark.location!.coordinate
print("Lat: \(coordinates.latitude) -- Long: \(coordinates.longitude)")
//added code
result = "Lat: \(coordinates.latitude) -- Long: \(coordinates.longitude)"
lat = coordinates.latitude
long = coordinates.longitude
}
})
print("\(lat)")
print("\(long)")
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView()
}
}
struct Item: Identifiable {
let id = UUID()
let text: String
}
//LocationButton
final class ContentViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 40, longitude: 120), span: MKCoordinateSpan(latitudeDelta: 100, longitudeDelta: 100))
let locationManager = CLLocationManager()
override init() {
super.init()
locationManager.delegate = self
}
func requestAllowOnceLocationPermission() {
locationManager.requestLocation()
}
func locationManager( _ _manager:CLLocationManager, didUpdateLocations locations: [CLLocation]){
guard let latestLocation = locations.first else {
// show an error
return
}
DispatchQueue.main.async{
self.region = MKCoordinateRegion(
center: latestLocation.coordinate,
span:MKCoordinateSpan(latitudeDelta:0.05, longitudeDelta:0.05))
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
}
The updated error is on line 50 now. Might be because of all of the new functions that I have. Also is the firebase function correct? I would like to make sure that it correct too.
I am working in Swift trying to update an organization struct that will need to hold a latitude and longitude. I created a mutating function in the struct that will update the latitude and longitude based on the organization organization struct’s address. I got it to work, but the issue is that when I call the mutating function, I need to manually enter the variable name with the .latitude and .longitude. Is there a way that I can pass the variable struct’s name automatically and reference the .latitude and .longitude without calling the specific variable name with it so I can make it more usable? I included an example below with my code. Thanks for your help!
import UIKit
import PlaygroundSupport
import CoreLocation
PlaygroundPage.current.needsIndefiniteExecution = true
struct organization {
var name: String
var address: String
var latitude: CLLocationDegrees = 0 //default setting for latitude
var longitude: CLLocationDegrees = 0 //default setting for longitude
mutating func getCoordinateFrom(completion: #escaping(_ coordinate: CLLocationCoordinate2D?, _ error: Error?) -> () ) {
CLGeocoder().geocodeAddressString(address) { placemarks, error in
completion(placemarks?.first?.location?.coordinate, error)
}
}
}
struct Coordinates {
var latitude: Double
var longitude: Double
}
//create an wildernessLodge variable of type organization
var wildernessLodge = organization(name: "Disney's Wilderness Lodge", address: "901 Timberline Dr, Orlando, FL 32830")
wildernessLodge.getCoordinateFrom { coordinate, error in
guard let coordinate = coordinate, error == nil else { return }
wildernessLodge.latitude = coordinate.latitude
wildernessLodge.longitude = coordinate.longitude
print("update 1 \(wildernessLodge)")
}
I'm a bit confused by your code. Why is getCoordinateFrom marked as mutating? Perhaps you meant to write something like this.
mutating func getCoordinatesFromAddress() {
CLGeocoder().geocodeAddressString(address) { placemarks, error in
guard let coordinate = placemarks?.first?.location?.coordinate, error == nil else { return }
self.latitude = coordinate.latitude
self.longitude = coordinate.longitude
}
}
Now this function is mutating (it modifies self), and
wildernessLodge.getCoordinateFrom { coordinate, error in ... }
can be replaced with
wildernessLodge.getCoordinatesFromAddress()
The only reason to leave the getCoordinateFrom method is if, somewhere in your code, you intend to get coordinates from an address but not update the coordinates in the struct. I can't imagine a good reason to do that, so I would recommend replacing the getCoordinateFrom method with something else.
Alternatively, if you generally intend to set the coordinates right after creating a value of this type, you might want to consider something like this.
init(name: String, address: String) {
self.name = name
self.address = address
CLGeocoder().geocodeAddressString(address) { placemarks, error in
guard let coordinate = placemarks?.first?.location?.coordinate, error == nil else { return }
self.latitude = coordinate.latitude
self.longitude = coordinate.longitude
}
}
or
init(name: String, address: String) {
self.name = name
self.address = address
self.getCoordinatesFromAddress()
}
Then, you could create an organization using organization(name: name, address: address) and the coordinates would automatically be set correctly.
If neither of these are satisfactory, maybe you should create two different structs to capture the behavior you want.
struct Organization {
var name: String
var address: String
func withCoordinates(completion: #escaping(_ coordinate: CLLocationCoordinate2D?, _ error: Error?) -> () ) {
CLGeocoder().geocodeAddressString(address) { placemarks, error in
completion(placemarks?.first?.location?.coordinate, error)
}
}
}
struct OrganizationWithCoordinates {
var name: String
var address: String
var latitude: CLLocationDegrees
var longitude: CLLocationDegrees
init(from organization: Organization) {
self.name = organization.name
self.address = organization.address
organization.withCoordinates { coordinate, error in
guard let coordinate = coordinate, error == nil else { return }
self.latitude = coordinate.latitude
self.longitude = coordinate.longitude
}
}
}
I would prefer an approach like this, but I like having lots of types.
Finally, as noted in the comments, if you are really just concerned with brevity, you can replace
var latitude: CLLocationDegrees
var longitude: CLLocationDegrees
with
var coordinates: CLLocationCoordinate2D
var latitude: CLLocationDegrees { coordinates.latitude }
var longitude: CLLocationDegrees { coordinates.longitude }
and then replace
wildernessLodge.latitude = coordinate.latitude
wildernessLodge.longitude = coordinate.longitude
with
wildernessLodge.coordinates = coordinate
In fact, you should feel free to combine any of these approaches.
Edit: As pointed out, these solutions do not work as-is. The fundamental tension is trying to work with CLGeocoder's async method synchronously. One solution is to use a class instead of a struct. The other approach is to use a modification of the withCoordinate method above:
struct Organization {
var name: String
var address: String
func withCoordinate(callback: #escaping (OrganizationWithCoordinate?, Error?) -> Void) {
CLGeocoder().geocodeAddressString(self.address) { placemarks, error in
if let coordinate = placemarks?.first?.location?.coordinate, error == nil {
let orgWithCoord = OrganizationWithCoordinate(name: self.name, address: self.address, latitude: coordinate.latitude, longitude: coordinate.latitude)
callback(orgWithCoord, nil)
} else {
callback(nil, error)
}
}
}
}
struct OrganizationWithCoordinate {
var name: String
var address: String
var latitude: CLLocationDegrees
var longitude: CLLocationDegrees
}
Organization(name: "Disney's Wilderness Lodge", address: "901 Timberline Dr, Orlando, FL 32830").withCoordinate { orgWithCoord, error in
guard let orgWithCoord = orgWithCoord, error == nil else {
print("Error")
return
}
print(orgWithCoord)
}
This embraces the async nature of CLGeocoder.
Another solution could be to force CLGeocoder to be synchronous using DispatchSemaphore as follows. I don't think these work correctly in Playgrounds, but this should work in an actual app.
struct Organization {
var name: String
var address: String
var latitude: CLLocationDegrees
var longitude: CLLocationDegrees
init(name: String, address: String) throws {
self.name = name
self.address = address
var tempCoordinate: CLLocationCoordinate2D?
var tempError: Error?
let sema = DispatchSemaphore(value: 0)
CLGeocoder().geocodeAddressString(address) { placemarks, error in
tempCoordinate = placemarks?.first?.location?.coordinate
tempError = error
sema.signal()
}
// Warning: Will lock if called on DispatchQueue.main
sema.wait()
if let error = tempError {
throw error
}
guard let coordinate = tempCoordinate else {
throw NSError(domain: "Replace me", code: -1, userInfo: nil)
}
self.longitude = coordinate.longitude
self.latitude = coordinate.latitude
}
}
// Somewhere in your app
let queue = DispatchQueue(label: "Some queue")
queue.async {
let wildernessLodge = try! Organization(name: "Disney's Wilderness Lodge", address: "901 Timberline Dr, Orlando, FL 32830")
DispatchQueue.main.async {
print(wildernessLodge)
}
}
Here, a new queue to do Organization related work is created to avoid locking up the main queue. This method creates the least clunky-looking code in my opinion, but probably is not the most performant option. The location APIs are async for a reason.
I'm trying to fetch data from Firestore. I've already got the following code but how do I properly append to shelters?
Current error:
Value of type '[String : Any]' has no member 'title'
class FirebaseSession: ObservableObject {
#Published var shelters: [Shelter] = []
let ref = Firestore.firestore().collection("shelters")
getShelters() {
ref.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let value = document.data()
let shelter = Shelter(id: Int(value.id), title: value.title, image: value.image, availableSpaces: value.available, distance: value.distance, gender: value.gender)
self.$shelters.append(shelter)
}
}
}
}
}
class Shelter {
var id: Int
var title: String
var image: String
var availableSpaces: Int
var distance: Double
var gender: String?
init?(id: Int, title: String, image: String, availableSpaces: Int, distance: Double, gender: String?) {
if id < 0 || title.isEmpty || image.isEmpty || availableSpaces < 0 || distance < 0 {
return nil
}
self.id = id
self.title = title
self.image = image
self.availableSpaces = availableSpaces
self.distance = distance
self.gender = gender
}
}
EDIT:
let shelter = Shelter(id: value["id"] as? Int ?? -1, title: value["title"] as? String ?? "", image: value["image"] as? String ?? "", available: value["available"] as? Int ?? -1, distance: value["distance"] as? Double ?? -1, gender: value["gender"] as? String ?? "")
let shelter = Shelter(id: Int(value.id), title: value.title, image: value.image, availableSpaces: value.available, distance: value.distance, gender: value.gender)
Here value is of type [String:Any]. So you cant do value.title . You need to do value["title"] as? String ?? "" and Similarly for id,image,distance,etc.
So the final code becomes:
let shelter = Shelter(id: Int(value["id"], title: value["title"], image: value["image"], availableSpaces: value["available"], distance: value["distance"], gender: value["gender"])
Downcast it accordingly.
UPDATE
replace your code with this
if let shelter = Shelter(id: value["id"] as? Int ?? -1, title: value["title"] as? String ?? "", image: value["image"] as? String ?? "", available: value["available"] as? Int ?? -1, distance: value["distance"] as? Double ?? -1, gender: value["gender"] as? String ?? "") {
self.shelters.append(shelter)
} else {
print("provided data is wrong.")
}
There are a number of issues with the original code:
Instead of implementing a function to fetch the shelters, the following snippet in your code creates a computed property - not sure this is what you intended:
getShelters() {
...
}
I'd recommend replacing this with a proper function.
No need to use a class for your data model - especially as you seem to be using SwiftUI.
Instead of mapping the fetched documents manually (and having to deal with nil values, type conversion etc. yourself, I'd recommend using Firestore's Codable support.
I've written about this extensively in my article SwiftUI: Mapping Firestore Documents using Swift Codable - Application Architecture for SwiftUI & Firebase | Peter Friese.
Here's how your code might look like when applying my recommendations:
struct Shelter: Codable, Identifiable {
#DocumentID var id: String?
var title: String
var image: String
var availableSpaces: Int
var distance: Double
var gender: String?
}
class FirebaseSession: ObservableObject {
#Published var shelters = [Shelter]()
private var db = Firestore.firestore()
private var listenerRegistration: ListenerRegistration?
deinit {
unsubscribe()
}
func unsubscribe() {
if listenerRegistration != nil {
listenerRegistration?.remove()
listenerRegistration = nil
}
}
func subscribe() {
if listenerRegistration == nil {
listenerRegistration = db.collection("shelters").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.shelters = documents.compactMap { queryDocumentSnapshot in
try? queryDocumentSnapshot.data(as: Shelter.self)
}
}
}
}
}
Note that:
we're able to do away with the constructor for Shelter
the shelter property will now be automatically updated whenever a shelter is added to the shelter collection in Firestore
the code won't break if a document doesn't match the expected data structure
I marked the Shelter struct as identifiable, so that you can directly use it inside a List view. #DocumentID instructs Firestore to map the document ID to the respective attribute on the struct.