Leave a rating/review
Notes: 07. Wrapping Delegate With Continuation
At the time of recording, Xcode 14 flags a runtime error in the location delegate continuation. If this happens to you, use Xcode 13 for the rest of Part 1.
Refresh your browser to make sure the course server is running or restart the server in Terminal. Continue with your project from the previous episode or open the starter project for this episode.
Build and run the app and login.
You’ll implement the show-location button in this episode and the next.
In this episode, you’ll use a manual continuation to make the Core Location manager delegate work with Swift concurrency.
Share Location
In ChatView, locate the first Button in the HStack.
Button(action: {
Task {
do {
try await model.shareLocation()
} catch {
lastErrorMessage = error.localizedDescription
}
}
}, label: {
Image(systemName: "location.circle.fill")
.font(.title)
.foregroundColor(Color.gray)
})
This button’s action calls the model’s shareLocation() method.
Managing authorizations
Jump to shareLocation() in BlabberModel. Add a location property:
let location: CLLocation =
try await withCheckedThrowingContinuation { [weak self] continuation in
}
This is a manual continuation. It’s useful for converting delegate methods into asynchronous functions. Let’s take a look at the API.
There are two flavors of manual continuation: CheckedContinuation and UnsafeContinuation. The first does runtime checks, and the second one doesn’t. You’ll use CheckedContinuation in these episodes. There are unsafe equivalents for everything you’ll do.
You can get a checked continuation or a checked throwing continuation.
withCheckedContinuation(_:): Wraps the closure and gives you a checked continuation back. withCheckedThrowingContinuation(_:): Wraps a throwing closure. Use this when you need error handling.
You must resume the continuation once — and exactly once. Enforcing this rule is the difference between checked and unsafe continuations. You resume a continuation with one of the following methods:
- resume(): Resumes the suspended task without a value.
- resume(returning:): Resumes the suspended task and returns the given value.
- resume(throwing:): Resumes the suspended task, throwing the provided error.
- resume(with:): Resumes with a Result containing a value or an error.
Now, back to your location property:
let location: CLLocation =
try await withCheckedThrowingContinuation { [weak self] continuation in
}
In this closure, you’ll create a location manager delegate and you’ll inject continuation into it. Then you’ll be able to use continuation in the delegate methods.
In this app, you want to share the user location only once, when the user taps the location button. But the standard CoreLocation manager doesn’t have a callback API that does this, so you’ll implement your own delegate type and code the logic to stop location updates after the first one comes through.
ChatLocationDelegate
In the Utility group, open ChatLocationDelegate.
You’ll add two methods here to handle location updates and location errors. There’s already a CLLocationManager to feed your proxy delegate with any updates.
Add a private optional continuation property:
// First, create a type alias
typealias LocationContinuation = CheckedContinuation<CLLocation, Error>
private var continuation: LocationContinuation?
Your delegate holds onto the continuation until it receives a location, so you need to store it in a property.
Next, create an initializer for injecting the continuation:
init(continuation: LocationContinuation) {
self.continuation = continuation
// call super.init() so you can set self as the delegate
super.init()
manager.delegate = self
// then the location manager can request authorization
manager.requestWhenInUseAuthorization()
}
Now, go back to BlabberModel to finish the closure in shareLocation():
self?.delegate = ChatLocationDelegate(continuation: continuation)
You create a ChatLocationDelegate, injecting the continuation provided by withCheckedThrowingContinuation(_:).
Now, back to ChatLocationDelegate to implement the first delegate method:
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus {
case .authorizedAlways, .authorizedWhenInUse:
manager.startUpdatingLocation()
case .notDetermined:
break
default:
// TODO: resume continuation instead of break
break
}
}
This method gets called immediately after the location manager is created, when the location permissions update. If the user grants permission, the location manager starts getting location data. If the user hasn’t responded to the permissions request, which would happen the first time they run the app, you do nothing. For all other cases, you want to throw an error,
Use your continuation to implement the default case:
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus {
case .authorizedAlways, .authorizedWhenInUse:
manager.startUpdatingLocation()
case .notDetermined:
break
default:
🟩
continuation?.resume(
throwing: "The app isn't authorized to use location data"
)
continuation = nil
🟥
}
}
You resume the continuation with an error. After resuming, you destroy the continuation because doing anything else with it is illegal.
Reminder: You must call a continuation’s resume(...) method exactly once from each code path. And then always set it to nil to make sure you don’t try to use it more than once.
Next, look at the delegate method that’s called when the user’s location updates:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else { return }
// TODO: resume continuation
}
The locations argument contains a list of CLLocation values. Here, it’s safe to return the first one to your own code.
Again, use the continuation to implement this:
func locationManager(
_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]
) {
guard let location = locations.first else { return }
🟩
continuation?.resume(returning: location) // returns, resumes etc: read below
continuation = nil // read next sentence below
🟥
}
Calling continuation?.resume(returning:) returns that first locations element and resumes the original code execution at the suspension point, back in shareLocation() in BlabberModel.
And destroy the continuation to make sure you can’t accidentally use it again.
Finally, use your continuation in the delegate method that handles errors:
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
// TODO: resume continuation
🟩
continuation?.resume(throwing: error)
continuation = nil
🟥
}
If the manager fails to fetch the device location, it calls this method on its delegate so you can update your app.
Using your delegate
Now, the complete workflow is in place: Once you set up the location manager with the delegate, it will try to fetch the current location and will use the injected continuation to return either a location or an error.
To review how this works, start in shareLocation() in BlabberModel:
- You create and store the delegate to make sure it isn’t immediately released from memory.
-
When
ChatLocationDelegateinitializes the manager, it calls the change authorization delegate method. - After the user grants permissions, the manager fetches the device location.
-
The manager calls the delegate with an array of
CLLocations. -
The delegate calls
continuationand resumes by returning the first availableCLLocation. -
The original call site
let location: CLLocation = try await withCheckedThrowingContinuation ...resumes execution, letting you use the returned location value.
To test the result, add a print statement at the very bottom of the function, outside the withCheckedThrowingContinuation closure:
print(location.description)
Now, delete the app from the simulator, then build and run. Click the location button in the Xcode debug toolbar and select one of the locations. I’ll pretend to be in India…
The location icon fills with color. Login, then tap the location button. Tap an Allow button.
Your location appears in the console. It’s just latitude and longitude. Paste it into a browser to see where it is…
Great work! You’ve integrated one of the oldest iOS APIs into your state-of-the-art Swift concurrency app.
In the next episode, to show your simulated address as a chat message, you’ll wrap a continuation around an API that uses a completion handler callback.