Swift New Concurrency Framework (Part 2)

Ali Akhtar
9 min readMay 10, 2022
https://developer.apple.com/news/?id=2o3euotz

It is mandatory to read part1 before continue this , OR if you have idea how async/await basic implementation

Apple released the beta version of Xcode 13.2. Besides the latest versions of the SDKs, it also brings backward compatibility for Swift’s new concurrency system. Until now, we could use async/await only with iOS 15, macOS Monterey, watchOS 8, and tvOS 15. Xcode 13.2 extends supported systems way back to iOS 13, macOS Catalina, watchOS 6, and tvOS 13. This is huge for developers as we still need to support earlier versions of the system. Let’s take a look at how to use it in practice.

As shown in Figure 1 we created a simple generic Network Manager to perform network request just pass model you expect in response and it in return fill that model , the magic Codable do normally, You have situation like NetworkManager in written in third party framework and they are very lazy to adopt new things so you can’t change their code

Figure 1

In Figure 2 you see how our code is using completion handler and we have minimum deployment is ios13 and we have Xcode 13.2, so our developers are very excited to use async-await because of it;s great benefits we saw in part1. The only problem is we can’t change NetworkManger class .Inshort client want to use new SDK

Figure 2

Continuations to convert completion handlers into async functions

  • Swift uses continuations to solve this problem, allowing us to create a bridge between older functions with completion handlers and newer async code.
  • As you can we have getFirstTodo older method that uses completion handler , we created async alternative firstTodo that internally calling our completion signature method and uses continuations to make a bridge , In short we refactor with no major impact on our older code , it might possible some will call getFirstTodo and new code will call firstTodo
  • Swift provides us with continuations, which are special objects we pass into the completion handlers as captured values. Once the completion handler fires, we can either return the finished value, throw an error, or send back a Result that can be handled elsewhere.
  • As you can see, starting a continuation is done using the withCheckedContinuation() function, which passes into itself the continuation we need to work with. It’s called a “checked” continuation because Swift checks that we’re using the continuation correctly, which means abiding by one very simple, very important rule: “Your continuation must be resumed exactly once. Not zero times, and not twice or more times — exactly once.:
  • By using continuation you observe we can make async alternative of our completion block method code , now callers can use it using async/await syntax
Figure 3
  • When we call firstTodo async function , it internally call getFirstTodo which have completion block . We need to return the result from the callback back to the places awaiting calls to the async firstTodo function. Not only that, those callers are in a suspended state. We need to make sure to resume them at the right point in time and with the right data so that they can get on with the rest of their work.
  • In part1 we showed you how Swift and the system cooperate to take care of suspend and resuming async code for us. The question is how in this case suspend/resume process works
  • When the async version of firstTodo it then calls getFirstTodo which from network fetch todo. At some later time, Network calls the completion handler and passes the result . This situation looks almost identical to the one we previously showed you earlier when our AsynchronousFunctionOne function asked the system, not Network, to resume a suspended async function call. (This things is important here when we discuss continuation)
  • All that’s missing is a bridge to await the completion handler and resume with the getFirstTodos results.
  • This pattern comes up all the time, and it has a name: a continuation. : (methods that take completion blocks).
  • The caller of the method awaits the result of the function call and provides a closure to specify what to do next. When the function call completes, it calls the completion handler to resume whatever the caller wanted to do with the result. This kind of cooperative execution is exactly the way async functions in Swift work. return await withCheckedContinuation { [weak self] (continuation: CheckedContinuation<Result<ToDo, Error> with this line it’s same like await we do but previous resume was done automatically , in our case with continuation the second link which is resume is to be done manually , Both case thread will suspened one or more time or never suspend A/C to system level
  • The withCheckedContinuation function lifts completion blocks without errors Swift functions. It has a counterpart called withCheckedThrowingContinuation for the situations where you know a function will throw an error. These functions are the way to gain access to a continuation value you can use to resume a suspended async function. This also builds the first part of the bridge by allowing us to await calls to getFirstTodo.
  • Let’s finish building the bridge. The continuation value provides a resume function into which we place the results from the completion handler. Not only that, but resume provides the missing link we need to unsuspend any calls awaiting the result of the persistentPosts function. And there, in one neat package, is our finished bridge from completion handlers to async functions.
Figure 4

Continuation Keep in Mind

Continuations provide a powerful way to manually take control over the execution of an async function, but there are some things to keep in mind.

  • Continuations have a simple but important contract. Resume must be called exactly once on every path. But don’t worry. Swift has your back here.
  • If the continuation is discarded without resume being called, the Swift runtime will log a warning since this will result in async calls never unsuspending as shown in Figure 5
  • If a continuation is resumed multiple times in the same function, however, this is a more serious error as it can corrupt program data. To combat this, the Swift runtime will detect attempts to call resume multiple times and will ensure a fatal error occurs at the second resumption point as shown in Figure 6
Figure 5
Figure 6
  • Many APIs are event driven. They provide delegate callbacks to notify our application at specific critical points and allow it to respond appropriately. In order to properly adopt async/await, we’ll have to store the continuation and resume it later.
  • As before, we create a checked continuation.Then we save it and kick off the work.To respect the API contract of checked continuations, we make sure to resume the active continuation, and finally nil it out so we’re protected from calling it more than once.
  • Always remember: the checked continuation value here represents the ability to manually resume any async calls to this API, so it must be called on all paths.
  • If your delegate API is called many times or not at all in certain circumstances, it is critical to resume any active continuations exactly once.
Figure 6

Recap await

  • await is where the magic happens. Whenever the program finds the await keywords, it has the option of suspending the function. It may or may not do so, and the decision is up to the system.
  • If the system does suspend the function, await will return control, not to the caller, but to the system. The system will then use the thread to perform other work until the suspended function is done. The statements below await will not be executed until it has finished. The system decides what’s important to execute, and at some point, it will return control back to you after it sees the awaited function has finished.
  • You can think of it as a traffic light. If you are driving down the road and you find a red light, chances are you will stop. But if it is 4 AM in the morning and there’s no cars coming you may just run it. ***

What you need to understand about await is that, if it does choose to suspend, nothing below it will execute until the system tells it to, and the system will use the thread to do other work.Every call to an async function, must be marked as await.

One final important note about await: It’s not guaranteed that the same thread that executed the code above it is the same one that will execute the code below it (commonly called the continuation).

Continuation Summary

  • A continuation is simply what happens after an async call. When you are using async/await, the continuation is easy to understand: Everything below an await call, is a continuation.
  • In this example, the keyword await will (may) trigger a data download task in a different thread. Everything underneath await (that is, starting on the line with a guard), is a continuation. on Code1
  • Continuations are not limited to the async/await APIs. When you are using closure-based async APIs, a continuation is everything called within your completion handlers.
Code 1

This is a closure version of the code above. Once again, the continuation starts at the guard. The main difference is the completion handler version has a flow that is harder to follow.

Code 2
  • Swift provides a few methods we can use to convert callback-based code into async/await: withCheckedContinuation and withCheckedThrowingContinuation. The difference between the two is the latter is used for code that throws errors. I call these methods explicit continuations.:
  • If you wanted to start your async/await migration with this method, the simplest way to do it would be by wrapping a call to downloadImageAndMetadata(for:imageNumber:completionHandler) inside the withCheckedThrowingContinuation method.
  • The magic behind this function occurs inside the withCheckedThrowingContinuation part. This function will give us a CheckedContinuation<T, E> where E: Error object that provides us with methods we need to call. In this example, the original version of downloadImageWithMetadata passes us a DetailedImage or an error, and we need to call the right resume method depending on what we get. If this method called us with a Result<DetailedImage, Error>, we could call .resume(with:) and pass it the result directly.
  • Continuations must be called exactly once, therefore there must be a continuation call within every branch of withCheckedThrowingContinuation. If you forget to call .resume, things could go awry. Luckily, Swift will let you know.
Code 3

Continue With Our Todo

As shown in Figure 7 we created withCheckedThrowingContinuation and now function throw error as well

Figure 7

Useful Links

https://developer.apple.com/videos/play/wwdc2021/10132/?time=1957

--

--

Ali Akhtar

Senior iOS Engineer | HungerStation | Delivery Hero