Darren
Darren

Reputation: 10408

Swift async/await in a for loop or map

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

Answers (1)

Rob
Rob

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

Related Questions