FREE TRIAL: Accelerate your app development career with Hacking with Swift+! >>

How to store continuations to be resumed later

Paul Hudson    @twostraws   

Many of Apple’s frameworks report back success or failure using multiple different delegate callback methods rather than completion handlers, which means a simple continuation won’t work.

As a simple example, if you were implementing WKNavigationDelegate to handle navigating around a WKWebView you would implement methods like this:

 func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
     // our work succeeded
 }

func webView(WKWebView, didFail: WKNavigation!, withError: Error) {
    // our work failed
}

So, rather than receiving the result of our work through a single completion closure, we instead get the result in two different places. In this situation we need to do a little more work to create async functions using continuations, because we need to be able to resume the continuation in either method.

To solve this problem you need to know that continuations are just structs with a specific generic type. For example, a checked continuation that succeeds with a string and never throws an error has the type CheckedContinuation<String, Never>, and an unchecked continuation that returns an integer array and can throw errors has the type UnsafeContinuation<[Int], Error>.

All this is important because to solve our delegate callback problem we need to stash away a continuation in one method – when we trigger some functionality – then resume it from different methods based on whether our code succeeds or fails.

I want to demonstrate this using real code, so we’re going to create an ObservableObject to wrap Core Location, making it easier to request the user’s location.

First, add these imports to your code so we can read their location, and also use SwiftUI’s LocationButton to get standardized UI:

import CoreLocation
import CoreLocationUI

Second, we’re going to create a small part of a LocationManager class that has two properties: one for storing a continuation to track whether we have their location coordinate or an error, and one to track an instance of CLLocationManager that does the work of finding the user. This also needs a small initializer so the CLLocationManager knows to report location updates to us.

Add this class now:

@MainActor
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
    var locationContinuation: CheckedContinuation<CLLocationCoordinate2D?, Error>?
    let manager = CLLocationManager()

    override init() {
        super.init()
        manager.delegate = self
    }

    // More code to come
}

Tip: Because that observable object is used with SwiftUI, I’ve marked it with the @MainActor attribute to avoid updating the user interface on a background thread. The @MainActor attribute is covered much later in this book.

Third, we need to add an async function that requests the user’s location. This needs to be wrapped inside a withCheckedThrowingContinuation() call, so that Swift creates a continuation we can stash away and use later.

Add this method to the class now:

func requestLocation() async throws -> CLLocationCoordinate2D? {
    try await withCheckedThrowingContinuation { continuation in
        locationContinuation = continuation
        manager.requestLocation()
    }
}

And finally we need to implement the two methods that might be called after we request the user’s location: didUpdateLocations will be called if their location was received, and didFailWithError otherwise. Both of these need to resume our continuation, with the former sending back the first location coordinate we were given, and the latter throwing whatever error occurred:

Add these last two methods to the class now:

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    locationContinuation?.resume(returning: locations.first?.coordinate)
}

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    locationContinuation?.resume(throwing: error)
}

So, by storing our continuation as a property we’re able to resume it in two different places – once where things go to plan, and once where things go wrong for whatever reason. Either way, no matter what happens our continuation resumes exactly once.

At this point our continuation wrapper is complete, so we can use it inside a SwiftUI view. Something like this ought to get you started:

struct ContentView: View {
    @StateObject var locationManager = LocationManager()

    var body: some View {
        LocationButton {
            Task {
                if let location = try? await locationManager.requestLocation() {
                    print("Location: \(location)")
                } else {
                    print("Location unknown.")
                }
            }
        }
        .frame(height: 44)
        .foregroundColor(.white)
        .clipShape(Capsule())
        .padding()
    }
}
Hacking with Swift is sponsored by Essential Developer

SPONSORED From August 2nd to 8th you can join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer!

Save your spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.