4

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?

5
  • So you just want to compact map [Property] to [CLLocationCoordinate2D]? I'm guessing what you have shown is 3 of your attempts to solve the same problem (and not 3 separate problems)? Commented Feb 6, 2022 at 12:16
  • Yes. I've shows 3 different attempts and the errors they give. Because my coordinates var is async due to geocodeAddressString being async. I want to get an array of coordinates to then place as map annotations. Commented Feb 6, 2022 at 12:18
  • I'm not sure why an answer was deleted, but luckily I managed to screen grab it before I had to go out and it was deleted as it does in fact do the job. Commented Feb 6, 2022 at 16:26
  • I deleted it because I had made some assumptions about your situation that are probably incorrect. You probably want the mapped coordinates to be in the same order as the properties, right? Using a TaskGroup wouldn't guarantee that order. I was not paying attention to the general context of what you are doing (adding annotations) and didn't consider the order as important. Commented Feb 6, 2022 at 16:32
  • Ah yes. I quickly realised I needed these linked/members of the Property itself to use it's other properties. I think I may need to look at having the Property compute the coordinates on init() Commented Feb 6, 2022 at 16:37

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you for this. So many useful things i've taken away from it. Regarding my loop for x in xx as you suggest works fine whereas the xx.forEach I was using does not, so that was an easy fix. I will add some caching to make sure the geocoding isn't performed once it's already successfully returned.
I also hadn't considered making my Property conform to MKAnnotation itself and had implemented a subclass to do the work. Thx

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.