简体   繁体   中英

How do I use async / await in swift?

Overview: I'm geocoding a user input address to use with mapkit and I am unable to wrap my head around using async / await to manage the delay.

I have a SwiftUI view with a textfield that calls a function onSubmit to start the geocoding process ...

                        TextField("Address ...", text: $searchString)
                        .onSubmit {
                            Task {
                                await switchToMap(address: searchString)
                            }
                        }

My switchToMap function ...

    func switchToMap(address: String) async {
    Task {
      locationManager.convertAddress(address: address)
    }
    if let coordinates = locationManager.addressCoordinates {
        locationManager.jumpToPoint(location: coordinates)
    }else{
        print("Something went wrong")
    }
}

LocationManager functions (as relevant)

    func getCoordinates(addressString: String, completionHandler: @escaping(CLLocationCoordinate2D, NSError?) -> Void) {
    let geocoder = CLGeocoder()
    geocoder.geocodeAddressString(addressString) { (placemarks, error) in
        if error == nil {
            if let placemark = placemarks?[0] {
                let location = placemark.location!
                completionHandler(location.coordinate, nil)
                return
        }
            print(error?.localizedDescription ?? "")
            completionHandler(kCLLocationCoordinate2DInvalid, error as NSError?)
    }
}
}

func convertAddress(address: String){
    getCoordinates(addressString: address) { (location, error) in
        if error != nil {
            // Error Handler
            print("Error converting address: \(error?.localizedDescription ?? "")")
            return
        }
        DispatchQueue.main.async {
            self.addressCoordinates = location
            //self.jumpToPoint(location: location)
        }
    }
}

So, how can I make use of async / await to make all this work? I fully expect that there are several issues with this code.

Let us consider CLGeocoder method geocodeAddressString first. That uses a traditional completion handler pattern. To use that in an async method, we need to wrap that in a withCheckedThrowingContinuation :

let geocoder = CLGeocoder()

func placemarks(for string: String) async throws -> [CLPlacemark] {
    try await withCheckedThrowingContinuation { continuation in
        geocoder.cancelGeocode() // cancel prior one if any

        geocoder.geocodeAddressString(string) { placemarks, error in
            guard
                error == nil,
                let placemarks = placemarks?.filter( { $0.location != nil }),
                !placemarks.isEmpty
            else {
                continuation.resume(throwing: error ?? CLError(.geocodeFoundNoResult))
                return
            }

            continuation.resume(returning: placemarks)
        }
    }
}

Note, I pulled the CLGeocoder out of the method and instead made it a property so that I could implement the “cancel prior request, if any” logic.

Also note that I am just fetching the array of CLPlacemark rather than the coordinates, but if you want the coordinates of the first one, you can then do:

func coordinate(for string: String) async throws -> CLLocationCoordinate2D {
    let placemark = try await placemarks(for: string)
        .first { $0.location != nil }

    guard let coordinate = placemark?.location?.coordinate else {
        throw CLError(.geocodeFoundNoResult)
    }

    return coordinate
}

Having shown how to wrap the completion-based method in a continuation, we should note that the CLGeocoder allows cancelation, but the above placemarks(for:) implementation will not responded to the cancelation. To do that, we need to wrap the above in a withTaskCancellationHandler :

func placemarks(for string: String) async throws -> [CLPlacemark] {
    try await withTaskCancellationHandler {
        geocoder.cancelGeocode()
    } operation: {
        try await withCheckedThrowingContinuation { continuation in
            geocoder.cancelGeocode() // cancel prior one if any

            geocoder.geocodeAddressString(string) { placemarks, error in
                guard
                    error == nil,
                    let placemarks = placemarks,
                    !placemarks.isEmpty
                else {
                    continuation.resume(throwing: error ?? CLError(.geocodeFoundNoResult))
                    return
                }

                continuation.resume(returning: placemarks)
            }
        }
    }
}

You might not need cancelation at this point, but it is simple and one of those things you should probably add for any cancelable process.


For more information about refactoring old code to adopt Swift concurrency, see WWDC 2021 video Swift concurrency: Update a sample app .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM