Records some concepts about Swift concurrency and some usage code.
async/await
enables us to manage long-running, asynchronous operations that are executed in the background.
Define a function with async
keyword:
func fetchData() async throws -> Data? {
// perform data request ...
}
If the asynchronous operation does not throw any error, you can define the function without throws
keyword, like the following:
func fetchData() async -> Data {
// perform data request ...
}
Call a async
function which may throw error:
Task {
do {
let data = try await fetchData()
print("Fetched \(data).")
} catch {
print("Fetching data failed with error \(error)")
}
}
Note: A
Task
provide a asynchronous context to callasync
function.
Call a asyc
function which will not throw any error:
Task {
let data = try await fetchData()
print("Fetched \(data).")
}
Leverage with continuation to generate async/await
function, Swift
provide four continuations:
withCheckedThrowingContinuation
withCheckedContinuation
withUnsafeThrowingContinuation
withUnsafeContinuation
Assume you have function with closure-based handler like the following :
func fetchData(_ completion: @escaping (Data?) -> Void) {
// perform async request ...
completion(nil)
}
First we can use withCheckedContinuation
method:
func fetchData() async -> Data? {
return await withCheckedContinuation { continuation in
fetchData { data in
continuation.resume(returning: data)
}
}
}
Throwing methods can use the withCheckedThrowingContinuation
method.
Second we can use withUnsafeContinuation
method:
func fetchData() async -> Data? {
return await withUnsafeContinuation { continuation in
fetchData { data in
continuation.resume(returning: data)
}
}
}
Throwing method can use the withUnsafeThrowingContinuation
method.
Keep in mind about continuation:
- Only call the continuation's
resume
once. No more, no less. - Throwing closure must use
withCheckedThrowingContinuation
orwithUnsafeThrowingContinuation
.
What is the difference between withCheckedContinuation
and withUnsafeContinuation
?
-
They work in the exact same way.
-
withUnsafeContinuation
does not check any potential errors, which means mistake will not be caught early. -
withCheckedContinuation
check some potential errors.
A Task
represent a unit of asynchronous work, and give us access to a concurrent context in which we can cal async
marked APIs to perform various operations in the background.
A sample task like the following:
let imageTask = Task { () -> UIImage? in
let imageURL = URL(string: "https://example/random")!
// Check for cancellation before the network request.
try Task.checkCancellation()
let (imageData, _) = try await URLSession.shared.data(from: imageURL)
// Check for cancellation after the network request
// to prevent starting our heavy image operations.
try Task.checkCancellation()
let image = UIImage(data: imageData)
// Perform image operations since the task is not cancelled.
return image
}
Since Swift 5.5 a new type declaration keyword has been added that is actor
.
Actors in Swift protect their state from data races, and using them allows the compiler to give us helpful feedback while writing applications.
It prevent data races by creating synchronized access to its isolated data. Before Actors, we would create the same result using all kinds of locks.
Here is an example to declare a actor
type:
actor Room {
let roomNumber = "101"
var visitorCount: Int = 0
func visit() -> Int {
visitorCount += 1
return visitorCount
}
}
To access data in actor
type, you need in the async/awit
context, below is an example:
func test() async {
let room = Room()
let visitCount = await room.visit()
print(visitCount)
print(await room.visitorCount)
}
Below is an example to define your own actor:
@globalActor actor MyActor {
static let shared = MyActor()
private init() {}
var value: Int = 0
func increase() {
value += 1
}
}
@MyActor func bar(actor: MyActor) async {
print(await actor.value)
print(await MyActor.shared.value)
}
Task {
let myActor = MyActor.shared
await myActor.increase()
await bar(actor: myActor)
}
A MainActor is a globally unique actor who performs tasks on the main thread.
@MainActor
can be used in class , properties and functions.
@MainActor
final class MyViewModel {
}
final class MyViewModel {
@MainActor var images: [UIImage] = []
@MainActor func fetchImage() -> {
Task {
do let images = try await loadImages()
images = images
} catch {
images = []
}
}
}
Use MainActor directly
Task {
await MainActor.run {
/// do some operation on main thread
}
}