In the last chapter, we learned about the async and await keywords to get started with the new concurrency system in Swift. We migrated a project that was using closure-based calls into async/await code. We did so by rewriting the original implementations of the methods to adapt them to the new system. But in the real world, you may not have the luxury of writing such implementations from scratch due to time constraints, or worse, technical difficulties. For these scenarios, we have Continuations, which will allow to “wrap” existing closure-based calls into async/await – and even delegate-based ones!
Continuations are useful, because not only will you be able to provide async/await versions of your own closure-based code , but also for third-party libraries and frameworks. This will help you create a consistent codebase that only uses async/await instead of closure-based calls when it makes sense to do so.
Not all closure-based code is necessarily so because it deals with concurrency and/or multithreading. There are multiple calls in both Swift and Apple’s SDKs that take closures and have nothing to do with concurrency. For example, the popular methods of collections, filter, map, and reduce, take closures to operate on their elements. Keep this in mind because it’s very hard to have projects that don’t have closures of any kind, and that’s fine. async/await aims to eliminate closure-based calls for concurrency-related tasks only.
Understanding Continuations
We have briefly touched the concepts of Continuations in previous chapters, and we will work through this chapter keeping that same meaning.
In short, a Continuation is whatever happens after an async call is finished. When we are using async/await , a continuation is simply everything below an await call. If you are using closure-based code , the continuation is the code written within your completion handlers. And if you are using delegate-based code , the continuation would be whatever methods can be called after an action is done, such as the original UIImagePickerController’s imagePickerController(didFinishPickingMediaWithInfo:) method.
The new concurrency system allows us to “convert” a continuation in the shape of closure-based code and even delegate-based code into async/await.
To better understand this short – but important – concept, we will go back to the original version of the Social Media App. We will use the version that doesn’t have async/await to illustrate this concept.
Converting closure-based calls into async/await
Original implementation for fetchUserInfo(completionHandler:)
Using withCheckedThrowingContinuation
Listing 3-2 essentially wraps closure-based code within a Checked Continuation. withCheckedThrowingContinuation – and other variants we will explore in a bit – is a method that provides a closure with a parameter. The parameter it gives you is the actual continuation, and you are responsible for resuming it when your asynchronous call with the closure is finished. In Listing 3-1, the continuation is of type CheckedContinuation<T, E>, meaning that we can resume the continuation with either an object or an error. This makes perfect sense because anything with a network call always has a chance of failing.
(1) will call withCheckedThrowingContinuation . This is an async function that can throw. For this reason, we call it with try await, and the function signature is similar to the async version of this code we saw in Chapter 2. When calling this method, we need to pass it a closure that will provide us with a continuation object we need to resume after it’s done.
(2) will call the closure-based version of fetchUserInfo(completionHandler:) as you would call it in any other context.
(3) is where the magic happens. If fetchUserInfo(completionHandler:) finishes successfully and we have a UserInfo object, we will resume the continuation by calling continuation.resume(returning: userInfo). If we get an error, we will instead call continuation.resume(throwing: error). When calling the continuation.resume(returning:) method, it will cause the caller of the async version of fetchUserInfo() to return the resulting object, and calling continuation.resume(throwing:) will cause it to throw an error instead. In the end, the caller of the method in Listing 3-2 will behave the same as the caller in Listing 2-10 all the way back in Chapter 2. Keep in mind that when using continuations, you must call the continuation at some point exactly once. You cannot forget to call it, and you cannot call it more than once. If you forget to call it, you will be in a deadlocked state.
withCheckedContinuation
withUnsafeThrowingContinuation
withUnsafeContinuation
Use withCheckedContinuation when the original closure-based code does not give you back an error of any kind.
The unsafe variants – withUnsafeThrowingContinuation and withUnsafeContinuation – are like their Checked counterparts. The main difference is, when working with Checked Continuations , Swift will help protect you by logging errors when any violations are encountered – calling continuations more than once or forgetting to call them completely. Unsafe continuations do not have these features, but they could be slightly faster. In general, I do not recommend you use Unsafe continuations , but be aware that you can use them interchangeably in most situations.
Converting delegate-based code into async/await
Converting delegate-based code into async/await is a more involved process since these types of APIs may call different delegate methods depending on what kind of events are happening. This will create naturally “jumpy” code. While the calls may happen in the same thread, they can be unpredictable because of the way they call their methods. For example, the ContactsUI framework offers an object, CNContactPickerViewController , which notifies you when the user chooses a contact in one method, and if they cancel the selection, a different method will be called.
For this section, download the “Chapter 3 - Contact Picker (Base Project)” project.
The ContactPicker Project
CNContactPickerViewController delegate methods
When our “Select Contact” button is tapped, the promptContact() method will be called. This will create, configure, and show a CNContactPickerDelegate. If the user cancels the operation, the contactPickerDidCancel(_) method will be called, setting the label to red with the text “Cancelled”. If the user selects a contact, the contactPicker(_:didSelect) method will be called instead, setting the label text to the contact name and its color to black. This is a simple example, but it illustrates the complexity of delegate-based calls.
Delegate-based APIs can be very messy, especially if they have a lot of possible method calls. We can create a more streamlined API for our callers by creating a wrapping object around CNContactPickerViewControllerDelegate and its methods.
Wrapping delegate-based calls in async/await alternatives
We will now proceed to create such a wrapper. We will modify the “Chapter 3 - ContactPicker (Base Project)” project. You can download the “Chapter 3 - ContactPicker (Async-Await)” project to see the final result.
Original implementation for promptContact()
A new implementation for promptContact()
In Listing 3-5, we safely unwrap an optional, because if the user tapped Cancel instead of selecting a contact, we will have no contact information to show. You could alternatively throw an error when the user cancels the selection, but because it’s the only possible failure reason in this project, we will not use errors.
Starting ContactPicker
We will use this object to wrap all the logic of the contact picker. It will be responsible for showing the contact picker UI and returning the result to us.
Basic properties
The viewController property will present the UI. We need to store the picker to show it when we call promptPicker(). Finally, because there are two different delegate methods , we need to store the checked continuation , so we can reference it when either delegate method is called.
The initializer for ContactPicker
The initializer takes a single property (the ViewController responsible from showing the picker). The picker is internal, so there’s no need to expose it. We will initialize the continuation when promptContact() is tapped.
The async version of pickContact()
We are marking this method with the @MainActor and not the whole class. This is because the ContactPicker class could be doing all sorts of different things in different threads, but we only care about the MainActor in this method because it interacts with the UI by presenting the contact picker modally.
The body of withCheckedContinuation does nothing but assign the continuation to our contactContinuation property. Again, we need to store the continuation somehow because we can resume it from two different delegate methods.
Resuming the continuation from two different delegate methods
These methods are very straightforward. Both methods will resume the continuation, either passing a contact when selecting it, or passing in nil if no contact is selected (the selection has been cancelled). Assigning the contactContinuation property to nil after resuming it will ensure that we cannot call the continuation more than once.
The final implementation for pickContact()
ViewController is now significantly smaller. Tapping the Choose Contact button should behave exactly as it did before we changed it all to use async/await.
While it is wonderful that we managed to offer an async/await version for a delegate-based call, you should consider if it’s really necessary to do so. Sometimes, the complexity of the delegate-based calls (CoreBluetooth, Core Location) will not be worthy to offer async/await versions for them. With that said, it’s a good exercise to work on to better understand how continuations work.
Supporting async/await in iOS 13 and 14
We mentioned this before, but it’s worth mentioning it again: While with Xcode 13.3 you can use async/await , Apple doesn’t offer any APIs that use them unless you compile with the iOS 15 SDK or above.
Xcode’s suggestion – to use the availability checks directly – sounds good in theory, but if you let Xcode fix it for you, the project will be in a worse state than it is now. Because it will wrap all our async calls in availability checks, the project will do nothing in iOS 13 and iOS 14 after you get it to run.
The solution is to use the availability check , but in extensions for URLSession and LAContext. We will fix this project without touching UserAPI and UserProfileViewModel. We will need to fix some SwiftUI issues later, but we don’t have to touch the logic code, making it easy to add async/await code without breaking any functionality. You can download the complete and fixed project as well, called “Chapter 2 - Social Media App (Async-Await)(iOS 14)(Fixed)”.
Backporting evaluatePolicy(_:localizedReason) async to iOS 14
The @available check will ensure that this piece of code can only be run on iOS 13 and iOS 14 . Older versions of iOS do not support async/await, so we will just exclude them altogether. When the deployment target is iOS 15, we will get a warning that reads “This method is no longer necessary. Use the API available in iOS 15.” When we start targeting iOS 15, we will be able to completely remove this extension, and the code will just work without having to do any further changes.
The continuation here allows us to smoothly convert a closure-based call into an async/await call that already exists in the SDK, but when targeting iOS 15 and up only. Instead of returning an error, we convert the method into a throwing one.
Backporting the data(from:) method to iOS 13 and iOS 14
There’s not much difference with the code in Listing 3-13. Perhaps the most interesting part here is that the return type is a tuple of type (Data, Response), but in general, exposing this method to iOS 14 and 13 was not complicated. Do note that we are force-unwrapping the returning data and response properties. The reason we do so is because if you look at the API’s original async signature, they are not marked as optionals in the tuple, therefore it is safe to assume that if we do not get an error, these two variables are going to have non-nil values. If you doubt this, feel free to mark the tuple elements as optionals.
If you try to compile the project now, we still get two errors, but these errors have nothing to do with async/await anymore. The errors we need to fix are in UserProfileView , so open the “UserProfileView.swift” file.
Creating an alternative view in case the device is not on iOS 15
Finally, the final error we need to fix has to do with prominent styles for buttons being introduced in iOS 15 . The easiest way to fix is to remove the .buttonStyle(.borderedProminent) line from UserProfileView.
And that’s it! You have successfully ported two async calls that didn’t exist in iOS 14 and that are native to iOS 15 , and if you try the compile the app now, it should work without an issue. If you want to test the iOS 14 and iOS 13 compatibility, make sure you run it in a simulator with one of those iOS versions. If you have none, you can go to the “Window” menu on Xcode, followed by “Organizer”, and you can add new simulators with different iOS versions there.
Note that unfortunately you cannot automatically backport all async/await code to iOS 14 and lower for free. You need to manually create extensions and write the continuation method by hand. Keep that in mind if you have a lot of closure-based code you would like to backport.
Summary
In this chapter, we explored what continuations are, and we learned how we can use them to convert closure-based code into async/await code without having to write new implementations for the methods they intend to replace from scratch. We also learned how we can provide an async API for those calls that use delegates by creating wrapper objects around the delegate methods . Finally, we learned how we can use continuations to backport async/await code to iOS 14 and iOS 13 – we can’t go lower because this feature is only available on iOS 13, 14, and 15.
Exercises
- 1.
Provide async/await versions for the requestPermissions and requestData functions using checked continuations. You should not remove the original implementations.
- 2.
Change the implementation of ContentViewViewModel.swift so it uses the async/await versions of those methods instead of the closure-based ones.
The solutions can be found in the “Chapter 2 Exercise - Solved Continuations” project.