What's New in Swift 5.9

Updated May 23, 2023#swift-versions#swift#ios

The Swift language has a formal process for proposing and accepting changes to the language, known as the Swift Evolution Process. Following proposals have been implemented and assigned to upcoming Swift 5.9. There will be more changes until official release.



  1. SE-0388Convenience Async[Throwing]Stream.makeStream methods

This feature adds a new static method makeStream on AsyncStream and AsyncThrowingStream that returns both the stream and the continuation, which make the stream’s continuation easier to access.

let (stream, continuation) = AsyncStream.makeStream(of: Int.self)

await withTaskGroup(of: Void.self) { group in
  group.addTask {
    for i in 0...9 {
      continuation.yield(i)
    }
    continuation.finish()
  }

  group.addTask {
    for await i in stream {
      print(i)
    }
  }
}
  1. SE-0380if and switch expressions

This feature introduces the ability to use if and switch statements as expressions, for the purpose of:

  • Returning values from functions, properties, and closures (either implicit or explicit)
  • Assigning values to variables
  • Declaring variables
let bullet =
  if isRoot && (count == 0 || !willExpand) { "" }
  else if count == 0 { "- " }
  else if maxDepth <= 0 { "▹ " }
  else { "▿ " }

public static func width(_ x: Unicode.Scalar) -> Int {
  switch x.value {
    case 0..<0x80: 1
    case 0x80..<0x0800: 2
    case 0x0800..<0x1_0000: 3
    default: 4
  }
}

For an if or switch to be used as an expression, it would need to meet these criteria:

  • Each branch of the if, or each case of the switch, must be a single expression.
  • Each of those expressions, when type checked independently, must produce the same type.
  • In the case of if statements, the branches must include an else.
  • The expression is not part of a result builder expression.
  • Pattern matching bindings may occur within an if or case.
  1. SE-0384Importing forward declared Objective-C interfaces and protocols

Forward declarations are very common in many existing Objective-C code bases, used often to break cyclic dependencies or to improve build performance. As it stands, the ClangImporter will fail to import any declaration that references a forward declared type in many common cases. This means a single forward declared type can render larger portions of an Objective-C API unusable from Swift.

This feature proposes the following representation for forward declared Objective-C interfaces and protocols in Swift:

// @class Foo turns into
@available(*, unavailable, message:This Objective-C class has only been forward declared; import its owning module to use it”)
class Foo : NSObject {}

// @protocol Bar turns into
@available(*, unavailable, message:This Objective-C protocol has only been forward declared; import its owning module to use it”)
protocol Bar : NSObjectProtocol {}

Permitted usages of these types are intentionally limited. You will be able to use Objective-C and C declarations that refer to these types without issue. You will be able to pass around instances of these incomplete types from Swift to Objective-C and vice versa.

Using the type itself directly in Swift is forbidden by the attached unavailable attribute. This is to keep the impact of the change small and to prevent unsound declarations, such as declaring a new Swift class that inherits from or conforms to such a type. You will also not be able to create new instances of these types in Swift.

The feature is gated behind a new frontend flag -enable-import-objc-forward-declarations. This flag is on by default for Swift version 6 and onwards.

  1. SE-0381[Throwing]DiscardingTaskGroup

This feature proposes discarding task group types:

  • DiscardingTaskGroup, withDiscardingTaskGroup
  • ThrowingDiscardingTaskGroup, withThrowingDiscardingTaskGroup

Similar to [Throwing]TaskGroup, however it discards results of its child tasks immediately. It is specialized for potentially never-ending task groups, such as top-level loops of http or other kinds of rpc servers to avoid memory leaks and performance issues when running for a long time or with many child tasks.

Discarding task group types work as follow:

  • They automatically cleans up its child Tasks when those Tasks complete.
  • They do not have a next() method, nor do they conform to AsyncSequence.

Cancellation and error propagation of discarding task group works the same way one comes to expect a task group to behave, however due to the inability to explicitly use next() to “re-throw” a child task error, the discarding task group types must handle this behavior implicitly by re-throwing the first encountered error and cancelling the group.

// GOOD, no leaks!
try await withThrowingDiscardingTaskGroup() { group in
    while let newConnection = try await listeningSocket.accept() {
        group.addTask {
            handleConnection(newConnection)
        }
    }
}
  1. SE-0374Add sleep(for:) to Clock

There is currently a slight imbalance between the sleep APIs for clocks and tasks, and it causes some troubles when dealing with Clock existentials.

let clock = ContinuousClock()

// Instant-based
try await Task.sleep(until: .now.advanced(by: .seconds(1)), clock: .continuous)
try await clock.sleep(until: .now.advanced(by: .seconds(1))

// Duration-based
try await Task.sleep(for: .seconds(1))
try await clock.sleep(for: .seconds(1)) // 🛑 API does not exist

The real problem occurs when dealing with Clock existentials. Because the Instant associated type is fully erased, and only the Duration is preserved via the primary associated type.

let clock: any Clock<Duration> = ContinuousClock()

// Instant-based
try await clock.sleep(until: .now.advanced(by: .seconds(1)) // 🛑

This release introduce a sleep(for:) method that only dealt with durations, and not instants, then we could invoke that API on an existential:

let clock: any Clock<Duration> = ContinuousClock()

// Duration-based
try await clock.sleep(for: .seconds(1)) // ✅ if the API existed
  1. SE-0392Custom Actor Executors

This proposal proposes a way to customize the executors of actors in Swift, which are responsible for scheduling and running tasks on actors. Executors are abstract types that conform to the Executor protocol and provide a enqueue(_:) method to submit tasks to be executed.

/// A service that can execute jobs.
public protocol Executor: AnyObject, Sendable {

  // This requirement is repeated here as a non-override so that we
  // get a redundant witness-table entry for it.  This allows us to
  // avoid drilling down to the base conformance just for the basic
  // work-scheduling operation.
  func enqueue(_ job: consuming ExecutorJob)

  @available(*, deprecated, message: "Implement the enqueue(_:ExecutorJob) method instead")
  func enqueue(_ job: UnownedExecutorJob)
}

This proposal also defines a SerialExecutor protocol, which is what actors use to guarantee their serial execution of tasks (jobs).

/// A service that executes jobs one-by-one, and specifically, 
/// guarantees mutual exclusion between job executions.
///
/// A serial executor can be provided to an actor (or distributed actor),
/// to guarantee all work performed on that actor should be enqueued to this executor.
///
/// Serial executors do not, in general, guarantee specific run-order of jobs,
/// and are free to re-order them e.g. using task priority, or any other mechanism.
public protocol SerialExecutor: Executor {
  /// Convert this executor value to the optimized form of borrowed
  /// executor references.
  func asUnownedSerialExecutor() -> UnownedSerialExecutor
  
  // Discussed in depth in "Details of 'same executor' checking" of this proposal.
  func isSameExclusiveExecutionContext(other executor: Self) -> Bool
}

extension SerialExecutor {
  // default implementation is sufficient for most implementations
  func asUnownedSerialExecutor() -> UnownedSerialExecutor {
    UnownedSerialExecutor(ordinary: self)
  }
  
  func isSameExclusiveExecutionContext(other: Self) -> Bool {
    self === other
  }
}

All actors implicitly conform to the Actor (or DistributedActor) protocols, and those protocols include the customization point for the executor they are required to run on in form of the the unownedExecutor property.