Back

Swift Concurrency

David Barkman
David BarkmanThursday, August 12, 2021
Photo of Mac laptop, partially closed, with screen aglow in a dark room.

As a developer, you will eventually write code that makes a call and takes some time to run. Network calls for example typically can take seconds to 10s of seconds to over a minute to run, in some extreme cases. There aren’t many instances when your users will want to wait that long for your app to respond and in an environment like a mobile device, the controlling operating system may think your app has crashed and shut it down. Threading and concurrency exist in most languages to handle this situation.

Concurrency is used to refer to the combination of asynchronous and parallel code. Asynchronous code allows for tasks to be suspended and resumed at a later point. This capability creates a more fluid user experience because it allows apps to complete short-term tasks like updating the UI while working on long-term tasks like retrieving data. Parallel code refers to multiple pieces of code running simultaneously. This gives users the ability to execute multiple tasks at the same time. The combination of asynchronous and parallel code, or concurrency, is essential to the function of modern mobile apps.

multi-colored yarn through a needle

WWDC, Upgrades for Swift!

Announced in June 2021, at Apple’s annual WWDC conference, Swift 5.5 and iOS 15 include support for async/await concurrency. Swift currently has methods for handling concurrency, the most prominent being closures, delegates and Notification Center with registered observers. Let’s look at each of these for just a moment, all three work fine and fill needed requirements, but sometimes make for a poor substitute for async/await.

The concurrency you know

Closures, also known as completion handlers, are little functions that are defined within one function and can then be passed to another function to be called sometime later. Crystal clear, right? Not really, especially for newer programmers. In my own apps, I typically use closures for calling external Rest APIs. It allows me to unblock the main thread and make asynchronous calls for data I want to display. My standard app build includes a method with a closure for the data task at the lowest level of the call, a method with a closure for the specific API endpoint I’m calling and a method with a closure in the view controller that needs the data. Following the path between three methods down to the actual network call to the API and then unwinding the call back up through the closures is non-trivial, even after writing it myself. If I go too long without reading the code, I sometimes have to stop for a minute and really think through the steps again.

Delegation is a design pattern that enables a class or structure to hand off (or delegate) some of its responsibilities to an instance of another type. I typically use delegates for passing data between view controllers. In a split view controller app, where you have at least two view controllers visible on screen at the same time, you will run into scenarios where one controller needs to send data or notifications to the other. Making one a delegate of the other, or both delegates of each other allows this communication.

Notification Center, the third type of concurrency commonly used in Swift lets you set up something like a public message board in your app. This works in three steps: first you initialize the board for the specific message, second you add an observer in the controller that will eventually act on the message and third, a separate process will post to the board that an event has occurred. I’ve used this pattern recently when a view controller was depending on an Oauth handshake to first take place before it could fetch data from an API. Once an asynchronous task completed the authorization, it posted to the message board that the session was initiated, then the view controller that had already made itself an observer was notified by Notification Center and was then able to run its fetch method, which then ran through the three layers of closures listed above. 😬

Visual Studio Code

So many patterns, so little time

All of these patterns can be used interchangeably, in many situations. Some situations fit one of the patterns better than others, but they work. I could use Notification Center for communicating between two controllers, but it’s cumbersome. Using closures for cross-controller communication would likely not work, so there is some crossover, but not complete. With the new async/await, you will mostly be using it for your calls to internal and external APIs, calls for data that take time to retrieve. Asynchronous functions, such as those using completion handlers, will become more readable and more correct when written in this new pattern with async/await.

Enough theory, let’s look at some example code for fetching and returning thumbnails for a UI.

Here’s the original version:

func fetchThumbnail(for id: String, completion: @escaping (UIImage?, Error?) -> Void) { let request = thumbnailURLRequest(for: id) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { completion(nil, error) } else if (response as? HTTPURLResponse)?.statusCode != 200 { completion(nil, FetchError.badID) } else { guard let image = UIImage(data: data!) else { completion(nil, FetchError.badImage) return } image.preparingThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in guard let thumbnail = thumbnail else { completion(nil, FetchError.badImage) return } completion(thumbnail, nil) } } } task.resume() }

Here’s the new version with async/await applied:

func fetchThumbnail(for id: String) async throws -> UIImage { let request = thumbnailURLRequest(for: id) let (data, response) = try await URLSession.shared.data(for: request) guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badID } let maybeImage = UIImage(data: data) guard let thumbnail = await maybeImage?.thumbnail else { throw FetchError.badImage } return thumbnail }

Besides having a single indent level and only six lines of code to read, compared to twenty, the new code only has one exit point, the return of the thumbnail. If an error is encountered, the code will throw a sensical error. In the original version, there are five separate exit points to debug, test and track. Bugs are more easily hidden in complex code and code that’s difficult to read and follow won’t get as thorough of a code review. Code with multiple exit points like the original tend to surface those edge case issues, something the developer didn’t think of in design and is now causing a bug in production.

Where to go from here?

Completion handlers and closures are still with Swift and still have their place. Hopefully, more as a single-level request and response and not as a series of sequential steps. Async/Await and the other new concurrency tools like Asynchronous Sequences, Continuations and Actors bring a needed upgrade to Swift that will result in more readable, easily testable, less bug-prone code that just does what it says. Learn more by visiting the Swift Concurrency page on the Apple Developer portal. Apple has also published an article and WWDC video, all free for anyone, covering refactoring an app with async/await and other new, concurrency tools on the Apple Developer portal.

Share this post

twitterfacebooklinkedin

Related Posts:

Interested in working with us?

Give us some details about your project, and our team will be in touch with how we can help.

Get in Touch