The biggest features in Swift 5.5 all revolve around its new and improved concurrency features. There’s actors, async/await, and more. With these features you are wondering whether async/await will replace Combine eventually.
For years completion handlers has been used for asynchronous programming, but these APIs are hard to use. This gets particularly problematic when many asynchronous operations are used, error handling is required, or control flow between asynchronous calls gets complicated.
This version introduces async
functions allowing you to compose complex logic involving asynchronous operations using the normal control-flow mechanisms. The compiler is responsible for translating an asynchronous function into an appropriate set of closures and state machines.
A call to a value of async function type (including a direct call to an async
function) introduces a potential suspension point. Any potential suspension point must occur within an asynchronous context (e.g., an async
function). Furthermore, it must occur within the operand of an await
expression.
func loadWebResource(_ path: String) async throws -> Resource
func decodeImage(_ r1: Resource, _ r2: Resource) async throws -> Image
func dewarpAndCleanupImage(_ i : Image) async throws -> Image
func processImageData() async throws -> Image {
let dataResource = try await loadWebResource("dataprofile.txt")
let imageResource = try await loadWebResource("imagedata.dat")
let imageTmp = try await decodeImage(dataResource, imageResource)
let imageResult = try await dewarpAndCleanupImage(imageTmp)
return imageResult
}
Learn more: SE-0296, kodeco.com, avanderlee.com, hackingwithswift.com
async let
bindingsThis version introduces a simple way to create child tasks and await their results using async let
declarations, similar to let
declarations, however they can only appear in asynchronous functions/closures.
func chopVegetables() async throws -> [Vegetables] {}
func marinateMeat() async -> Meat {}
func preheatOven(temperature: Int) async -> Oven {}
func makeDinner() async throws -> Meal {
async let veggies = chopVegetables()
async let meat = marinateMeat()
async let oven = preheatOven(temperature: 350)
let dish = Dish(ingredients: await [try veggies, meat])
return try await oven.cook(dish, duration: .hours(3))
}
The child task begins running as soon as the async let
is encountered. By default, child tasks use the global, width-limited, concurrent executor, in the same manner as task group child-tasks do.
Reading a variable defined by a async let
is treated as a potential suspension point, and therefore must be marked with await
.
Task groups work best for spreading out computation of same-typed operations, while async let
works best for heterogeneous result processing and variable initialization.
Learn more: SE-0317
This version introduces globally-unique actor identified by a type, which becomes a custom attribute.
A global actor is a type (can be a struct, enum, actor, or final class) that has the @globalActor attribute, implicitly conform to the GlobalActor protocol, which contains a static property named shared
that provides a shared instance of an actor.
@globalActor
public struct SomeGlobalActor {
public actor MyActor { }
public static let shared = MyActor()
}
A primary motivator of global actors is to apply the actor model to the state and operations that can only be accessed by the main thread. The main actor is a global actor that describes the main thread:
@globalActor
public actor MainActor {
public static let shared = MainActor(...)
}
Note that global actors are not restricted to global functions or data. You can mark members of types and protocols as belonging to a global actor as well.
Learn more: SE-0316
This version adds two new types AsyncStream
and AsyncThrowingStream
to support implementing an AsyncSequence
interface, each include a nested Continuation
type for you to send values, errors, and “finish” events.
By default, every element yielded to an AsyncStream ’s continuation is buffered until consumed by iteration, can be configed with maxBufferedElements
.
func buyVegetables(
shoppingList: [String],
onGotVegetable: (Vegetable) -> Void,
onAllVegetablesFound: () -> Void,
onNonVegetable: (Error) -> Void
)
// Returns a stream of veggies
func findVegetables(shoppingList: [String]) -> AsyncThrowingStream<Vegetable> {
AsyncThrowingStream { continuation in
buyVegetables(
shoppingList: shoppingList,
onGotVegetable: { veggie in continuation.yield(veggie) },
onAllVegetablesFound: { continuation.finish() },
onNonVegetable: { error in continuation.finish(throwing: error) }
)
}
}
Learn more: SE-0314, avanderlee.com, donnywals.com, kodeco.com
Previously instance methods (and properties, and subscripts) on an actor type are always actor-isolated. No other functions can be actor-isolated and there is no way to make an instance method not be isolated.
This version generalizes the notion of actor-isolated functions such that any function can choose to be actor-isolated by indicating which of its actor parameters is isolated, as well as making an instance declaration on an actor not be actor-isolated at all.
A function can become actor-isolated by indicating that one of its parameters is isolated.
func deposit(amount: Double, to account: isolated BankAccount) {
assert(amount >= 0)
account.balance = account.balance + amount
}
Instance declarations on an actor type implicitly have an isolated self
. However, one can disable this implicit behavior using the nonisolated
keyword:
actor BankAccount {
nonisolated let accountNumber: Int
var balance: Double
}
Learn more: SE-0313
Previously, you have to rely on thread-local or queue-specific values as containers to associate information with a task and carry it across suspension boundaries.
This version introduces the semantics of values which are local to a task. These values set in a task cannot out-live the task, solving many of the pain points relating to un-structured primitives such as thread-locals, as well as aligning this feature closely with Swift’s take on structured concurrency.
A task-local must be declared as a static stored property, and annotated using the @TaskLocal property wrapper.
enum MyLibrary {
@TaskLocal
static var requestID: String?
}
Learn more: SE-0311, hackingwithswift.com
This version allows async
, throws
, or both of these effect specifiers to be marked on a read-only computed property or subscript:
class BankAccount {
var lastTransaction: Transaction {
get async throws {
guard manager != nil else {
throw BankError.notInYourFavor
}
return await manager!.getLastTransaction()
}
}
subscript(_ day: Date) -> [Transaction] {
get async {
return await manager?.getTransactions(onDay: day) ?? []
}
}
}
Learn more: SE-0310
#if
for postfix member expressionsSwift has conditional compilation block #if ... #endif
which allows code to be conditionally compiled depending on the value of one or more compilation conditions. The body of each clause must surround complete statements.
However, in some cases, especially in result builder contexts, demand for applying #if
to partial expressions has emerged. This version expands #if ... #endif
to be able to surround postfix member expressions.
baseExpr
#if CONDITION
.someOptionalMember?
.someMethod()
#else
.otherMember
#endif
Learn more: SE-0308
An actor is a reference type that protects access to its mutable state, and is introduced with the keyword actor
. Like other Swift types, actors can have initializers, methods, properties, and subscripts. They can be extended and conform to protocols, be generic, and be used with generics.
actor BankAccount {
let accountNumber: Int
var balance: Double
init(accountNumber: Int, initialDeposit: Double) {
self.accountNumber = accountNumber
self.balance = initialDeposit
}
func transfer(amount: Double, to other: BankAccount) async throws {
assert(amount > 0)
if amount > balance {
throw BankError.insufficientFunds
}
balance = balance - amount
await other.deposit(amount: amount)
}
}
Actor isolation is how actors protect their mutable state, the primary mechanism for this protection is by only allowing their stored instance properties to be accessed directly on self
.
Cross-actor reference to immutable state is allowed from anywhere in the same module as the actor is defined.
The second form of permissible cross-actor reference is one that is performed with an asynchronous function invocation. Such asynchronous function invocations are turned into “messages” requesting that the actor execute the corresponding task when it can safely do so.
Learn more: SE-0306, swiftbysundell.com
The relationship between a child task and a parent task describes a hierarchy that’s the structure in structured concurrency. All asynchronous functions run as part of an asynchronous task. Tasks can make child tasks that will perform work concurrently. This creates a hierarchy of tasks, and information can naturally flow up and down the hierarchy, making it convenient to manage the whole thing holistically.
A task group defines a scope in which one can create new child tasks programmatically. As with all child tasks, the child tasks within the task group scope must complete when the scope exits, and will be implicitly cancelled first if the scope exits with a thrown error.
func makeDinner() async throws -> Meal {
// Prepare some variables to receive results from our concurrent child tasks
var veggies: [Vegetable]?
var meat: Meat?
var oven: Oven?
enum CookingStep {
case veggies([Vegetable])
case meat(Meat)
case oven(Oven)
}
// Create a task group to scope the lifetime of our three child tasks
try await withThrowingTaskGroup(of: CookingStep.self) { group in
group.addTask {
try await .veggies(chopVegetables())
}
group.addTask {
await .meat(marinateMeat())
}
group.addTask {
try await .oven(preheatOven(temperature: 350))
}
for try await finishedStep in group {
switch finishedStep {
case .veggies(let v): veggies = v
case .meat(let m): meat = m
case .oven(let o): oven = o
}
}
}
// If execution resumes normally after `withTaskGroup`, then we can assume
// that all child tasks added to the group completed successfully. That means
// we can confidently force-unwrap the variables containing the child task
// results here.
let dish = Dish(ingredients: [veggies!, meat!])
return try await oven!.cook(dish, duration: .hours(3))
}
Learn more: SE-0304, kodeco.com
Asynchronous Swift code needs to be able to work with existing synchronous code that uses techniques such as completion callbacks and delegate methods to respond to events. Asynchronous tasks can suspend themselves on continuations which synchronous code can then capture and invoke to resume the task in response to an event.
func beginOperation(completion: (OperationResult) -> Void) {}
func operation() async -> OperationResult {
// Suspend the current task, and pass its continuation into a closure
// that executes immediately
return await withUnsafeContinuation { continuation in
// Invoke the synchronous callback-based API...
beginOperation(completion: { result in
// ...and resume the continuation when the callback is invoked
continuation.resume(returning: result)
})
}
}
Learn more: SE-0300
Using static member declarations to provide semantic names for commonly used values which can then be accessed via leading dot syntax is an important tool in API design. Currently, when a parameter is generic, there is no effective way to take advantage of this syntax.
This version lifts restrictions on accessing static members on protocols to afford the same call-site legibility to generic APIs.
// Existing SwiftUI APIs:
public protocol ToggleStyle { ... }
public struct DefaultToggleStyle: ToggleStyle { ... }
public struct SwitchToggleStyle: ToggleStyle { ... }
public struct CheckboxToggleStyle: ToggleStyle { ... }
extension View {
public func toggleStyle<S: ToggleStyle>(_ style: S) -> some View
}
// Possible SwiftUI APIs:
extension ToggleStyle where Self == DefaultToggleStyle {
public static var `default`: Self { .init() }
}
extension ToggleStyle where Self == SwitchToggleStyle {
public static var `switch`: Self { .init() }
}
extension ToggleStyle where Self == CheckboxToggleStyle {
public static var checkbox: Self { .init() }
}
// Leading dot syntax (using proposed solution):
Toggle("Wi-Fi", isOn: $isWiFiEnabled)
.toggleStyle(.switch)
Learn more: SE-0299
AsyncSequence
is a type providing asynchronous, sequential, and iterated access to its elements. It’s an asynchronous variant of the regular Sequence
we’re familiar with in Swift.
struct Counter : AsyncSequence {
let howHigh: Int
struct AsyncIterator : AsyncIteratorProtocol {
let howHigh: Int
var current = 1
mutating func next() async -> Int? {
// We could use the `Task` API to check for cancellation here and return early.
guard current <= howHigh else {
return nil
}
let result = current
current += 1
return result
}
}
func makeAsyncIterator() -> AsyncIterator {
return AsyncIterator(howHigh: howHigh)
}
}
for await i in Counter(howHigh: 3) {
print(i)
}
Learn more: SE-0298, avanderlee.com