What's New in Swift 5.5

Updated Feb 14, 2023#swift-versions#swift

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.



Async/await

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 bindings

This 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

Global actors

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

AsyncStream and AsyncThrowingStream

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

Improved control over actor isolation

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

Task local values

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

Effectful read-only properties

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 expressions

Swift 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

Actors

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

Structured concurrency

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

Continuations

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

Extending static member lookup in generic contexts

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

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