Reading time: 11 min
Here you can find recipes for common tasks you need when working with the new Swift 5.5 concurrency model.
Where applicable, there's before and after comparison that shows how things where done before async/await (mostly with GCD) and how they can be done now.
This list will be updated as Swift 5.5 gains wider adoption and as more recipes are concocted.
Table of contents
- Table of contents
- Basic async/await use-case
- Error handling
- Sequential execution
- Parallel execution
- Cooperative execution with TaskGroup
- Run async function in sync function
- Wrap sync code with callbacks into async with continuations
- Pause / delay / sleep an async function
- Return value from a Task
- Cancel a Task
- Do work on background thread
- Async computed properties
- Async sequence
- Generator
- Async stream
- Iterating over Combine Publishers
- Basic actor
- @globalActor
- @MainActor
- Resolving error: "Property 'X' isolated to global actor 'MainActor' can not be mutated from a non-isolated context"
- Resolving error: "Converting non-concurrent function value to '@Sendable () async -> Void' may introduce data races"
Basic async/await use-case
- Get rid of callbacks and the pyramid of doom.
- Just
return
values instead of passing them as callbacks arguments. - Overall make code easier to read, write and reason about.
Before:
func getFileNames(callback: @escaping ([String]) -> Void) {
// Use GCD to simulate asnyc background work
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
callback(["Doc1.txt", "Doc2.txt", "Doc3.txt"])
}
}
func getFileContent(for files: [String], callback: @escaping (String) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
callback(files.joined(separator: "\n"))
}
}
func use() {
getFileNames { fileNames in
getFileContent(for: fileNames) { content in
print(content)
}
}
}
After:
func getFileNames() async -> [String] {
["Doc1.txt", "Doc2.txt", "Doc3.txt"]
}
func getFileContent(for files: [String]) async -> String {
files.joined(separator: "\n")
}
func use() async {
let fileNames = await getFileNames()
let content = await getFileContent(for: fileNames)
print(content)
}
Tip: You can put before and after code side-by-side, and it'll compile, because the functions don't have the same signature.
Error handling
No more Result
, just throw
and then try/catch
:
Before:
func getFileNames(callback: @escaping (Result<[String], Error>) -> Void) {
// What a nice, stable function this is
if Int.random(in: 0...1) == 0 {
callback(.success(["Doc1.txt", "Doc2.txt", "Doc3.txt"]))
} else {
callback(.failure(SomeError.yepAnError))
}
}
func use() {
getFileNames { result in
switch result {
case .success(let fileNames):
print(fileNames)
case .failure(let error):
print(error.localizedDescription)
}
}
}
After:
func getFileNames() async throws -> [String] {
if Int.random(in: 0...1) == 0 {
return ["Doc1.txt", "Doc2.txt", "Doc3.txt"]
} else {
throw SomeError.yepAnError
}
}
func use() async {
do {
let fileNames = try await getFileNames()
print(fileNames)
} catch {
print(error.localizedDescription)
}
}
Of course, you can try?
or try!
as well - all the usual Swift error handling rules apply here as well.
async
always goes before throws
- but try
goes before await
.
Sequential execution
Calls to async functions execute sequentially by default, in the order in which they're listed:
// file1 is downloaded first, followed by file2, and then file3
let file1 = await downloadFile("File1.txt")
let file2 = await downloadFile("File2.txt")
let file3 = await downloadFile("File3.txt")
for file in [file1, file2, file3] {
printFileContent(file)
}
Parallel execution
Use async let
to make async calls execute in parallel:
async let file1 = downloadFile("File1.txt")
async let file2 = downloadFile("File2.txt")
async let file3 = downloadFile("File3.txt")
// the parallel downloads start at this `await` call
for file in await [file1, file2, file3] {
printFileContent(file)
}
Cooperative execution with TaskGroup
If you have multiple tasks (pieces of async code) that work together to produce a result, and which may have a hierarchical ordering to them, wrap them in a TaskGroup
. This is, in a way, similar to what you do in Combine with sequences of flatMap
and Publishers.Merge
:
func getFileNames() async -> [String] {
return ["Doc1.txt", "Doc2.txt", "Doc3.txt"]
}
func getFileContent(for fileName: String) async -> String {
await Task.sleep(UInt64.random(in: 1...3) * 1_000_000_000)
return "Content for \(fileName)"
}
func getAllFiles() async {
// combine tasks with a TaskGroup
let allContents = await withTaskGroup(of: String.self,
returning: String.self) { taskGroup in
// add tasks to the group
for fileName in await getFileNames() {
taskGroup.addTask(priority: nil) { () -> String in
await getFileContent(for: fileName)
}
}
// collect task results
var contents = [String]()
for await content in taskGroup {
contents.append(content)
}
return contents.joined(separator: "\n\n")
}
print(allContents)
}
Run async function in sync function
To run some async code in a non-async block of code, wrap it in a Task
:
func mySyncFunc() {
print("Starting async func")
Task {
await use()
}
print("More sync code")
}
This doesn't make the code run synchronously, though - it just allows you to call async code within sync code. The output of the function above is:
Starting async func
More sync code
Now I'll sleep for 5 seconds...
Up and running again!
The initialized Task
runs immediately, so there's no need to kick it off manually.
Wrap sync code with callbacks into async with continuations
If you have a sync function that uses callbacks that you can't directly convert into an async one (because it's in an external library, for compatibility reasons, etc.), you can wrap it with a continuation using someBackgroundTaskWrapper
. Once your callback is invoked, resume the execution of the async function by calling resume(returning:)
on the continuation parameter:
func someBackgroundTask(callback: @escaping (String) -> Void) {
callback("Some string")
}
func someBackgroundTaskWrapper() async -> String {
await withCheckedContinuation { cont in
someBackgroundTask { result in
cont.resume(returning: result)
}
}
}
If you need to propagate errors from the wrapped sync function, use withCheckedThrowingContinuation
and throw errors with resume(throwing:)
:
func getFileNames(callback: @escaping (Result<[String], Error>) -> Void) {
// What a nice, stable function this is
if Int.random(in: 0...1) == 0 {
callback(.success(["Doc1.txt", "Doc2.txt", "Doc3.txt"]))
} else {
callback(.failure(SomeError.yepAnError))
}
}
func getFileNamesWrapper() async throws -> [String] {
try await withCheckedThrowingContinuation { cont in
getFileNames { result in
switch result {
case .success(let fileNames):
cont.resume(returning: fileNames)
case .failure(let error):
cont.resume(throwing: error)
}
}
}
}
Continuations can automatically handle Result
callbacks using resume(with:)
, which allows us to write code from above as:
getFileNames { result in
cont.resume(with: result)
}
Continuations must be resumed, and they must be resumed exactly once.
Pause / delay / sleep an async function
If you want to pause the execution of the current code for a while, use Task.sleep
.
Before:
func use() {
print("Now I'll sleep for 5 seconds...")
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
print("Up and running again!")
}
}
After:
func use() async {
print("Now I'll sleep for 5 seconds...")
try? await Task.sleep(5_000_000_000)
print("Up and running again!")
}
Note that Task.sleep
takes in nanoseconds as parameter.
To make specifying sleep/delay intervals easier, add this helpful extension to your code:
extension TimeInterval {
var toNanoseconds: UInt64 {
UInt64(self * 1E9)
}
}
Which allows you to write:
try? await Task.sleep(5.toNanoseconds)
try? await Task.sleep(2.5.toNanoseconds)
Return value from a Task
Task
is basically a cancellable async block of code, which means it can return a value:
let task = Task { () -> String in
print("Doing some work")
return "Result of my hard work"
}
let taskResult = await task.value
print(taskResult)
You always need to disambiguate the Task
block, as Swift compiler can't infer it on its own.
Cancel a Task
Suppose you have a task:
let task = Task { ... }
Cancel it in outside code with:
task.cancel()
Inside the task, handle cancellation with checkCancellation
, which then throws a CancellationError
:
let task = Task {
print("Doing work")
do {
try Task.checkCancellation() // have I been cancelled in the meantime?
print("Nope, still going strong!")
} catch CancellationError {
print("Cancelled)
}
}
Or, you can mode the error handling to the call site:
let task = Task { () -> String in
print("Doing some work")
try Task.checkCancellation()
return "Result of my hard work"
}
let taskResult = try? await task.value
Do work on background thread
You shouldn't really think about async/await code in terms of threads, but if it's really important that you move your work off the main thread, use Task(priority:)
:
func test() async {
Thread.printCurrent() // runs on main thread
Task.detached(priority: .background) {
Thread.printCurrent() // runs off main thread
}
}
Which results in:
⚡️: <_NSMainThread: 0x600001c980b0>{number = 1, name = main}
🏭: com.apple.main-thread
⚡️: <NSThread: 0x600001c9c790>{number = 7, name = (null)}
🏭: None
You can find the Thread.printCurrent
function definition here.
Async computed properties
In Swift 5.5, it's also possible to declare async getters for computed properties:
var myProperty: Int {
get async {
try? await Task.sleep(3.toNanoseconds)
return 5
}
}
Async setters (set async
) aren't supported at the moment.
Async sequence
AsyncSequence
is just the same as regular Sequence
, except its values can come in asynchronously. It implements an AsyncIteratorProtocol
, of which you can think a bit as a Combine Publisher
, except this is easier to work with for simpler use cases.
Here's a sample iterator and sequence that simulate delays that might come in due to networking:
struct DelayedStrings: AsyncSequence {
typealias Element = String
struct DelayedIterator: AsyncIteratorProtocol {
private var internalIterator = ["Some", "strings", "here"].makeIterator()
mutating func next() async -> String? {
await Task.sleep(2_000_000_000)
return internalIterator.next()
}
}
func makeAsyncIterator() -> DelayedIterator {
DelayedIterator()
}
}
Using for await
for iterate an async sequence:
for await value in DelayedStrings() {
print(Date())
print(value)
}
which prints:
2021-08-23 08:22:04 +0000
Some
2021-08-23 08:22:07 +0000
strings
2021-08-23 08:22:09 +0000
here
Using other common sequence methods:
print(Date())
let contains = await DelayedStrings().contains(where: { $0 == "here" })
print(contains)
print(Date())
which prints:
2021-08-23 08:31:33 +0000
true
2021-08-23 08:31:39 +0000
Generator
Use AsyncSequence
to create a generator - an infinite iterator that produces a new value whenever it's invoked:
struct FibonacciSequence: AsyncSequence {
typealias Element = Int
struct FibonacciGenerator: AsyncIteratorProtocol {
private var a = 0
private var b = 1
mutating func next() async -> Int? {
let c = a
a = b
b = a + c
return c
}
}
func makeAsyncIterator() -> FibonacciGenerator {
FibonacciGenerator()
}
}
Async stream
There are two ways of looking at AsyncStream
(and its error-handling twin, AsyncThrowingStream
):
- They serve a similar purpose as continuations do, just for code that produces a sequence of values as opposed to just wrapping a single callback.
- They allow for implementing the same functionality as
AsyncSequence
offers without having to write a custom struct.
func delayedStrings() -> AsyncStream<String> {
let strings = ["Some", "strings", "here"];
return AsyncStream { continuation in
Task {
for string in strings {
await Task.sleep(2_000_000_000)
continuation.yield(string)
}
continuation.finish()
}
}
}
Then, you can consume it with for await
, just like AsyncSequence
:
for await value in delayedStrings() {
print(value)
}
Iterating over Combine Publishers
In Combine, every Publisher
has a values
property that conforms to AsyncSequence
, which means that you can iterate over publishers with for await
instead of using sink
or custom subscribers:
let publisher = [1, 2, 3, 4, 5]
.publisher
.delay(for: .seconds(3), scheduler: RunLoop.main)
.eraseToAnyPublisher()
for await value in publisher.values {
print(Date())
print(value)
}
This, expectedly, outputs:
2021-09-24 08:20:35 +0000
1
2021-09-24 08:20:38 +0000
2
2021-09-24 08:20:41 +0000
3
2021-09-24 08:20:44 +0000
4
2021-09-24 08:20:47 +0000
5
Basic actor
Here's a breakdown of basic actor functionality:
- Actors are reference types, like classes.
- Actors are thread/task safe.
- All actor methods and accessors are
async
by default, without having to specify it manually. Consequently, all their invocations must be marked withawait
. - Actor data can only be set/mutated inside the actor itself.
actor StringDatabase {
private var data = [String]()
func insert(_ string: String) {
data.append(string)
}
func query() -> [String] {
data
}
func delete(_ string: String) {
if let index = data.firstIndex(of: string) {
data.remove(at: index)
}
}
}
func use() async {
let db = StringDatabase()
await db.insert("A")
await db.insert("B")
await print(db.query())
await db.delete("A")
await print(db.query())
}
@globalActor
Global actors allow you to mark classes, properties and methods that are supposed to run on the same thread. Conformance to @globalActor
requires your type to be a singleton:
@globalActor struct NetworkingActor {
actor MyActor {
// custom implementation
}
static var shared = MyActor() // singleton
}
Then, you can mark code elements with it:
class MyViewModel {
// ...
@NetworkingActor
func loadData() async -> Data {
// ...
}
}
@MainActor
@MainActor
is a special global actor whose code is run on the main thread. Just like with other global actors, you can mark classes, properties or methods with it to indicate that they should run on the main thread:
```swift
class MyViewModel {
@MainActor
func renderData() {
// ...
}
@NetworkingActor
func loadData() async -> Data {
// ...
}
}
Resolving error: "Property 'X' isolated to global actor 'MainActor' can not be mutated from a non-isolated context"
You may encounter this error if you're changing @State
vars inside an async block, e.g, when using the pull-to-refresh compatibility component. The solution is to move the code to a function marked with @MainActor
:
@MainActor // HERE
func refreshAction() async {
await Task.sleep(3_000_000_000)
now = Date()
}
var body: some View {
RefreshableScrollView(action: refreshAction) {
// ...
}
}
Resolving error: "Converting non-concurrent function value to '@Sendable () async -> Void' may introduce data races"
You may encounter this error if you're changing @State
vars inside an async block, which is a parameter of a function, marked with @Sendable
in the function signature. The solution is to move the code to a function marked with @Sendable
:
@Sendable // HERE
func refreshAction() async {
await Task.sleep(3_000_000_000)
now = Date()
}
var body: some View {
RefreshableScrollView(action: refreshAction) {
// ...
}
}