Pre Swift 5.5, you used GCD to run asynchronous code via dispatch queues — an abstraction over threads.
GCD’s queue-based model worked well. However, it would cause issues, like:
Concurrency means that different pieces of code run at the same time.
The new concurrency model is tightly integrated with the language syntax, the Swift runtime and Xcode. It abstracts away the notion of threads for the developer. Its key new features include:
Simple example
SwiftUI views offer support for calling asynchronous functions. Also, while a function is concurrently executing, we can change the view without having it blocked.
In this short snippet, we'll integrate an async function that suspends its execution for a few seconds, and at the same time, we can use a button to increase a counter shown in the view.
Create a Service class with the following two functions:
class Service {
func fetchResult() async -> String {
await sleep(seconds: 5)
return "Result"
}
private func sleep(seconds: Int) async {
try? await Task.sleep(nanoseconds: 2000000000) // wait 2 seconds
}
}
The async keyword in the method’s definition lets the compiler know that the code runs in an asynchronous context. In other words, it says that the code might suspend and resume at will. Also, regardless of how long the method takes to complete, it ultimately returns a value much like a synchronous method does.
In ContentView, add two @State properties, as shown in the following code:
struct ContentView: View {
@State var value: String = ""
@State var counter = 0
let service = Service()
var body: some View {
VStack {
Text(value)
Text("\(counter)")
Button {
counter += 1
} label: {
Text("increment")
}
.buttonStyle(.bordered)
}
.task {
value = await service.fetchResult()
}
}
}
To update a view, SwiftUI requires that the code runs in the main thread. .task{} does this for us by moving the code execution from the background thread to the main thread.
So, the async keyword defines a function as asynchronous. await lets you wait in a non-blocking fashion for the result of the asynchronous function.
Routing code to the main thread
One of possible way to ensure your code is on the main thread is calling MainActor.run().
await MainActor.run {
... your UI code ...
}
MainActor is a type that runs code on the main thread. It’s the modern alternative to the well-known DispatchQueue.main, which you might have used in the past.
Fetching remote data in SwiftUI
One of the operations made easier by the async/await model is network operations. Fetching data from a network resource is an asynchronous operation by definition and until now, we had to manage it with the callback mechanism. iOS 15 adds an async/await interface to the URLSession class to embrace the new pattern.
Create the model objects to transform the JSON returned from the network call to Swift struct
struct Post: Codable {
let userId: Int
let id: Int
let title: String
let body: String
}
Create a Service struct to call the network request and decode the response into a Decodable model:
struct PostService {
private let decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}()
private func fetch<T: Decodable>(type: T.Type, from urlString: String) async -> T? {
guard let url = URL(string: urlString) else { return nil }
do {
let (data, _) = try await URLSession.shared.data(from: url)
return try decoder.decode(type, from: data)
} catch {
return nil
}
}
func fetchPosts() async -> [Post] {
await fetch(type: Post.self,
from: "https://jsonplaceholder.typicode.com/posts/") ?? []
}
}
Create a ViewModel class to manage the business logic
@MainActor
class PostViewModel: ObservableObject {
@Published var posts: [Post] = []
@Published var isLoading = false
let service = PostService()
func fetch() async {
isLoading = true
result = await service.fetchPosts()
isLoading = false
}
}
Since published properties update the UI, any changes that are made to them need to be run on the main thread. By marking a class with the @MainActor attribute, the Swift compiler will guarantee that all methods and properties on it are only ever called from the main actor.
Add a List view in ContentView to present the posts
struct ContentView: View {
@StateObject var vm = PostViewModel()
var body: some View {
List(vm.posts) { post in
Text(post.title)
}
.listStyle(.plain)
.task {
await vm.fetch()
}
}
}
Fetching remote data in UIKit
The NetworkService looks like this
class NetworkService {
let session: URLSession
init(session: URLSession = .shared) {
self.session = session
}
func get<T: Decodable>(url: URL) async throws -> T {
let urlRequest = URLRequest(url: url)
let result: (data: Data, response: URLResponse) = try await self.session.data(for: urlRequest)
let statusCode = (result.response as? HTTPURLResponse)?.statusCode
guard let code = statusCode, (200..<300) ~= code else {
throw NetworkError.serverError(statusCode: statusCode)
}
return try JSONDecoder().decode(T.self, from: result.data)
}
}
Let's define a manager that will do a network call
struct UserManager: AsyncAwaitUserService {
let networkService: NetworkService
init(networkService: NetworkService = NetworkService()) {
self.networkService = networkService
}
func getUser(id: Int) async throws -> User {
let url = URL(string: "https://www.server.com/user")!
.appendingPathComponent("\(id)")
return try await self.networkService.get(url: url)
}
}
We can use it with following snippet (somewhere in UIViewController)
let um = UserManager()
var userTask: Task<Void, Never>?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
userTask = Task {
do {
let user = try await um.getUser(id: 1)
updateUI(user)
} catch {
print("Request failed with error: \(error)")
}
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
userTask?.cancel()
}
Converting a completion block function to async/await
A continuation is an object that tracks a program’s state at a given point. The Swift concurrency model assigns each asynchronous unit of work a continuation instead of creating an entire thread for it. Concurrency model creates only as many threads as there are available CPU cores, and it switches between continuations instead of between threads, making it more efficient.
With await your current code suspends execution and creates a continuation that represents the entire captured state at the point of suspension. Next step it hands the thread and system resources over to the central handler, which decides what to do next. When the awaited function completes, your original code resumes, as long as no higher priority tasks are pending.
Manually creating continuations allows you to migrate your existing code gradually to the new concurrency model.
There are two continuation API variants:
CheckedContinuation. A mechanism to resume a suspended execution or throw an error. It provides runtime checks for correct usage and logs any misuse.UnsafeContinuation. An alternative to CheckedContinuation, but without the safety checks. Use this when performance is essential and you don’t need the extra safety.So, it's clear that async/await is the way that Apple wants us to develop concurrent code in Swift. But what if a framework we have to use still has a completion block-based interface? Swift 5.5 provides a simple way to convert these APIs to async await functions.
Let's take a completion block function for fetching a list of post, with the following signature
func fetchPosts(page: Int, completion: ([Post]) -> Void)
We use the withCheckedContinuation function to transform it into an async function with the following signature:
func fetchPostsAsync(page: Int) async -> [Post] {
return try await withCheckedContinuation { continuation in
self.fetchPosts(page: page) { result in
continuation.resume(with: result)
}
}
}
The withCheckedContinuation function suspends the current task, and then it calls the passed callback with a CheckedContinuation object.
Inside the callback, we call the completion block-based function, and when it finishes, we resume the execution of the task via the CheckedContinuation that withCheckedContinuation provided.
An important note here is that we must call continuation.resume() exactly once in the withCheckedContinuation block. If we forget to do it, our app would be blocked forever. If we do it twice, the app would crash.
Swift provides another function to deal with APIs that can return an error state. Let's say we have the following function:
func oldAPI(completion: (Result<[Post], Error>) -> Void)
We can use the withCheckedThrowingContinuation function to transform it into a function with the following signature
func newAPI() async throws -> [Post]
The withCheckedThrowingContinuation function follows the same pattern as the withCheckedContinuation, as you can see from the following code
func newAPI() async throws -> [Post] {
try await withCheckedThrowingContinuation { continuation in
oldAPI { result in
switch result {
case .success(let value):
continuation.resume(returning: value)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
Grouping asynchronous calls
Have a look at following code
let value1 = try await model.getValue1() let value2 = try await model.getValue2()
Both calls are asynchronous and could happen at the same time. However, by explicitly marking them with await, the call to getValue2() doesn’t start until the call to getValue1() completes.
Sometimes, you need to perform sequential asynchronous calls — like when you want to use data from the first call as a parameter of the second call.
Swift offers a special syntax that lets you group several asynchronous calls and await them all together.
do {
async let value1 = try model.getValue1()
async let value2 = try model.getValue2()
let (value1Result, value2Result) = try await (value1, value2)
} catch {
errorMessage = error.localizedDescription
}
An async let binding allows you to create a local constant that’s similar to the concept of promises in other languages.
To get the binding results, you need to use await. If the value is already available, you’ll get it immediately. Otherwise, your code will suspend at the await until the result becomes available.
To group concurrent bindings and extract their values, you have two options:
async let approach has one restriction, it can't run a variable number of tasks at the same time, we have to to await the results.
A Task Group is a form of structured concurrency designed to provide a dynamic amount of concurrency. With it, we can launch multiple tasks, launch them in a group, and have them execute all at the same time.
There are a few task group behaviors that we need to be aware of:
To create a task group, we either call withTaskGroup or withThrowingTaskGroup, depending on whether we’d like to have the option to throw errors within our tasks.
Let’s create a simple image loader that asynchronously returns a dictionary of images keyed by the URLs that they were downloaded
func loadImages(from urls: [URL]) async throws -> [URL: UIImage] {
try await withThrowingTaskGroup(of: (URL, UIImage).self) { group in
for url in urls {
group.addTask{
let image = try await self.loadImage(from: url)
return (url, image)
}
}
var images = [URL: UIImage]()
for try await (url, image) in group {
images[url] = image
}
return images
}
}
All child tasks will run concurrently and we have no control over when they will finish running. In order to collect the result of each child task, we must loop through the task group. Notice we are using the await keyword when looping to indicate that the for loop might suspend while waiting for the child task to complete. Every time when a child task returns, the for loop will iterate and update the images dictionary.
Canceling an async task
Canceling unneeded tasks is essential for the concurrency model to work efficiently.
If you write your async code inside a .task{} view modifier in SwiftUI, it will be responsible for automatically canceling your code when the view disappears. But the actions in some button aren’t in a .task{}, so there’s nothing to cancel your async operations.
To fix this issue, you’ll manually cancel your task. Start by adding a new state property
@State var someTask: Task<Void, Error>?
In someTask, you’ll store an asynchronous task that returns no result and could throw an error. Task is a type like any other, so you can also store it in your view, model or any other scope. Task doesn’t return anything if it’s successful, so success is Void; likewise you return an Error if there’s a failure.
Next, assign your task task to someTask
someTask = Task {
... code ...
}
This stores the task in someTask so you can access it later. Most importantly, it lets you cancel the task at will.
You’ll cancel the task when the user navigates back to the previous screen. Add following code in .onDisappear or viewWillDisappear.
// SwiftUI
.onDisappear {
someTask?.cancel()
}
// UIKit
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
someTask?.cancel()
}
Canceling someTask will also cancel all its child tasks — and all of their children, and so forth.