Reputation: 10408
I have this model:
struct ExactLocation {
var coordinates: CLLocationCoordinate2D? {
get async throws {
let geocoder = CLGeocoder()
let placemark = try await geocoder.geocodeAddressString(address)
let mark = MKPlacemark(placemark: placemark.first!)
return mark.coordinate
}
}
}
struct Property {
let exactLocation: ExactLocation?
}
I am trying to loop over an array of Property
to fetch all the coordinates using Swift 5.5 async/await.
private func addAnnotations(for properties: [Property]) async {
let exactLocations = try? properties.compactMap { try $0.exactLocation?.coordinates } <-- Error: 'async' property access in a function that does not support concurrency
let annotations = await properties.compactMap { property -> MKPointAnnotation? in
if let exactLocation = property.exactLocation {
if let coordinates = try? await exactLocation.coordinates {
}
}
} <-- Error: Cannot pass function of type '(Property) async -> MKPointAnnotation?' to parameter expecting synchronous function type
properties.forEach({ property in
if let exactLocation = property.exactLocation {
if let coordinates = try? await exactLocation.coordinates {
}
}
} <-- Error: Cannot pass function of type '(Property) async -> Void' to parameter expecting synchronous function type
}
So how can I iterate over this array with an async function?
Do I need to create an AsyncIterator
? The docs are quite confusing on this, how would I do this for this simple example?
Upvotes: 4
Views: 3972
Reputation: 438437
First, be careful about the number of requests that you perform. The docs say:
Send at most one geocoding request for any one user action.
If the user performs multiple actions that involve geocoding the same location, reuse the results from the initial geocoding request instead of starting individual requests for each action.
When you want to update the user’s current location automatically (such as when the user is moving), issue new geocoding requests only when the user has moved a significant distance and after a reasonable amount of time has passed. For example, in a typical situation, you should not send more than one geocoding request per minute.
And the old Location and Maps Programming Guide says:
The same
CLGeocoder
object can be used to initiate any number of geocoding requests but only one request at a time may be active for a given geocoder.
So, the whole idea of rapidly issuing a series of geolocation requests may be imprudent, and even if you were to do just a few, I would be inclined to avoid performing them concurrently. So, I would consider a simple for
loop, e.g.:
func addAnnotations(for addresses: [String]) async throws {
let geocoder = CLGeocoder()
for address in addresses {
if
let placemark = try await geocoder.geocodeAddressString(address).first,
let coordinate = placemark.location?.coordinate
{
let annotation = MKPointAnnotation()
annotation.title = placemark.name
annotation.coordinate = coordinate
// ...
// you might even want to throttle your requests, e.g.
//
// try await Task.sleep(nanoseconds: nanoseconds)
}
}
}
Technically, you could do the computed property approach. Now, I did not see address
in your model anywhere, but let’s imagine:
struct Property {
let address: String
var coordinate: CLLocationCoordinate2D? {
get async throws {
try await CLGeocoder()
.geocodeAddressString(address)
.first?.location?.coordinate
}
}
}
(Note the elimination of the forced unwrapping operator and the unnecessary instantiating of another placemark.)
Then you could do:
func addAnnotations(for properties: [Property]) async throws {
for property in properties {
if let coordinate = try await property.coordinate {
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate
...
}
}
}
I am not crazy about that approach (as we are hiding rate-limited CLGeocoder
requests with all sorts of constraints inside a computed property; if you access the same property repeatedly, duplicate geocoder requests will be issued, which Apple explicitly advises that we avoid). But the async
property technically works, too.
Often when dealing with annotations, we want to be able to interact with the annotation views on our map and know with which model object they are associated. For that reason, we would often keep some sort of cross reference between our annotations and our model objects.
If Property
was a reference type, we might use a MKPointAnnotation
subclass that kept a reference to the appropriate Property
. Or we might just make our Property
conform to MKAnnotation
, itself, eliminating the need for references between annotations and separate model objects. There are lots of ways to tackle this requirement, and I’m not sure we have enough information to advise you on the correct pattern in your case.
Upvotes: 2