Asynchronous programming is a common requirement of any iOS application. The ability to perform task on a separate thread and not disturbing or blocking the user interface is always considered a good practice. In iOS 15 and Swift 5.5 Apple introduced async/await feature, which allows developers to easily implement asynchronous tasks with increased clarity and less lines of code.
In this article, we are going to take a look at how you can use async/await, continuation and actors in your iOS application.
Before we jump into the new async/await features, we need to see how things are handled currently. It is by looking at the the past, we can appreciate the future.
For this example we are going to use JSONPlacefolder API to fetch all posts. The implementation of our client is shown below:
class Webservice {
func getAllPosts(completion: @escaping (Result<[Post], NetworkError>) -> Void) {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
completion(.failure(.badURL))
return
}
URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else {
completion(.failure(.badRequest))
return
}
let posts = try? JSONDecoder().decode([Post].self, from: data)
completion(.success(posts ?? []))
}.resume()
}
}
We all have implemented similar code in our application. The main issue with the above code is that you need to remember to return from the guard clauses. The other issue is the overall length of the code. For an operation like this, it turns out to be 10-15 lines of code.
In the next section, we are going to implement the same code but using the new async/await features.
Async and await is not something new, several programming languages including C#, and JavaScript have been using async/await for a very long time.
It is definitely a welcome change in Swift language and it will allow developers to write better and clear concurrency code. Now, let’s check the async/await implementation for the getAllPosts
function as implemented below.
func getAllPosts() async throws -> [Post] {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
throw NetworkError.badURL
}
let (data, _) = try await URLSession.shared.data(from: url)
return (try? JSONDecoder().decode([Post].self, from: data)) ?? []
}
The async/await version is not only fewer lines of code, but it is also much easier to understand. The function getAllPosts
is also decorated with throws, which indicates that the async function can throw an error. This is the reason we called the URLSession.shared.data
with try await
instead of just await.
You can only decorate the function with await, if it is marked with async.
The URLSession now exposes a new async function, which allows us to pass the request URL. The await keyword is required since the URLSession data function is marked with async. Whenever Swift compiler see async with a function it suspends the operation. This means the thread is now free to be used for something else. When the function finally returns after fetching data, Swift compiler picks up where it left off and returns the result to the user.
Apart from URLSession, async/await is also available in several other frameworks including HealthKit, CoreData, Notifications etc.
If you are using MVVM pattern then your view model will be responsible for calling the Webservice. Since, getAllPosts
is marked with async we need to await for the result in the view model function as shown below:
class PostListViewModel: ObservableObject {
@Published var posts: [PostViewModel] = []
func fetchPosts() async {
do {
let posts = try await Webservice().getAllPosts()
DispatchQueue.main.async {
self.posts = posts.map(PostViewModel.init)
}
} catch {
print(error)
}
}
}
Once we get the result, we assign it to the posts property which is marked with @Published
property wrapper. The Dispatch.main.async
is making sure that the @Published
property is set on the main thread.
In iOS 15, there is an easier way to make sure that properties and functions are called on the main thread. This is accomplished by using the @MainActor
property wrapper as shown below:
@MainActor
class PostListViewModel: ObservableObject {
@Published var posts: [PostViewModel] = []
func fetchPosts() async {
do {
let posts = try await Webservice().getAllPosts()
self.posts = posts.map(PostViewModel.init)
} catch {
print(error)
}
}
}
The @MainActor
is part of the new actor API introduced in Swift language. The @MainActor
makes sure that all properties and functions in PostListViewModel are called on the main thread. This means you don’t have to worry about using DispatchQueue.main.async
anymore.
Actors will be covered in more detail in the future articles.
Now, you can call fetchPosts
from inside your view as shown in the implementation below:
struct ContentView: View {
@StateObject private var postListVM = PostListViewModel()
var body: some View {
List(postListVM.posts, id: \.id) { post in
Text(post.title)
}.task {
await postListVM.fetchPosts()
}
}
}
One thing you will notice is the use of task
modifier. The task modifier is introduced in iOS 15 and it used to perform a task when the view is loaded. The task modifier is cancelled when the view disappears. The task modifier is an async closure, which means you can call and await inside the body of the task modifier.
At this point you might be wondering that what will happen to the onAppear function of the view. According to the conversations with Apple engineers, currently there are no plans to depreciate onAppear.
The async keyword is not only for functions, but it can also be decorated on properties and even closures. Take a look at the following code, which calls fetchPosts
on a button click event.
Button {
await postListVM.fetchPosts()
} label: {
Text("Fetch Posts")
}
The above code is not going to compile. Can you see the problem?
The problem is that we are using await to call the fetchPosts
function but the context is not marked with async. This means we cannot use await, unless we make the context async. But how can we decorate the button click function with async? The answer is to use async closures. This is shown below:
Button {
async {
await postListVM.fetchPosts()
}
} label: {
Text("Fetch Posts")
}
The async closure is executed immediately and it allows us to satisfy the requirement of performing an await inside the async context. This is really useful as it allows you to call async functions from non-async contexts.
Async and await provides a great way to perform asynchronous tasks but what about your legacy callback implementations. You can use the new continuation
feature in iOS 15 to invoke your callbacks using async/await.
At the start of this article we implemented getAllPosts
using callbacks. If we want to use that function as async/await we could wrap it in a continuation closure as shown below:
func getAllPosts() async throws -> [Post] {
return try await withCheckedThrowingContinuation { continuation in
getAllPosts { result in
switch result {
case .success(let posts):
continuation.resume(returning: posts)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
You can see that we are wrapping the call to getAllPosts
inside the withCheckedThrowingContinuation
closure. This means that once your getAllPosts
call completes, continuation can either resume or throw an error.
The continuation API comes in different flavors as shown below:
You can read more about continuation on Swift evolution repository here.
The continuation concept is similar to Promises and Futures. I hope that Apple make the continuation API more intuitive by giving it a developer friendly name.
Continuation is a great way to wrap your existing functions and give them the ability to be called using async/await syntax. In JavaScript, you can also use the Promise.all
and Promise.any
syntax to make sure that the Promise is only resumed if all or any of the tasks are completed. This functionality is currently not available in Swift language but I hope they add it in the future versions.
iOS 15 also introduces the support for asynchronous sequences using AsyncSequence
protocol. AsyncSequence
is useful when you are interested in functions that return many values over times.
AsyncSequence
is already available in several Apple APIs including URL
, URLSession
and Notification
etc.
In order to iterate an AsyncSequence
, you can use the for-await in
syntax. In the code below we are iterating through the bytes.lines
sequence using the new await in
keyword in Swift.
let request = URLRequest(url: url)
let (bytes, _) = try await URLSession.shared.bytes(for: request)
for try await line in bytes.lines {
print(line)
}
AsyncSequence
will allow us to wait on each element, instead of the getting the entire result.
Here is another example of AsyncSequence
using NotificationCenter
.
for await note in NotificationCenter.default.notifications(named: .itemCreated) {
// Use item.
}
The above code will iterate through all the .itemCreated
notifications asynchronously and process them.
Async/await is one of the more anticipated new feature of the Swift language. It will drastically change how we write concurrent code and it will allow developers to implement asynchronous code in a clear concise manner.
If you liked this article and want to support my work then check out courses on Udemy.