Swift 6 brings 23 new proposals to the language, totalling 10,922 lines and 73,896 words. This is a digest of what you need to know as a developer. Get comfortable, this is a long read.

Proposals in Swift 6

git clone https://github.com/swiftlang/swift-evolution/
cd swift-evolution

# 23 proposals
grep -lr 'Status.*Swift 6' . | wc -l

# 10922 lines and 73896 words
grep -lr 'Status.*Swift 6' . | sort | xargs wc -lw
   70     471  0220-count-where.md
  454    3242  0270-rangeset-and-collection-operations.md
  179    1477  0301-package-editing-commands.md
  223    1509  0405-string-validating-initializers.md
  203    1180  0408-pack-iteration.md
  342    3023  0409-access-level-on-imports.md
 1844   11930  0410-atomics.md
 2073   11083  0414-region-based-isolation.md
  312    2324  0415-function-body-macros.md
   99     695  0416-keypath-function-subtyping.md
  803    5782  0417-task-executor-preference.md
  421    2970  0418-inferring-sendable-for-methods.md
  580    3873  0420-inheritance-of-actor-isolation.md
  271    2372  0421-generalize-async-sequence.md
  196    1057  0422-caller-side-default-argument-macro-expression.md
  182    1713  0423-dynamic-actor-isolation.md
  206    1720  0424-custom-isolation-checking-for-serialexecutor.md
  493    2998  0426-bitwise-copyable.md
  472    3959  0428-resolve-distributed-actor-protocols.md
  863    6031  0431-isolated-any-functions.md
  325    2080  0432-noncopyable-switch.md
  257    2059  0434-global-actor-isolated-types-usability.md
   54     348  0435-swiftpm-per-target-swift-language-setting.md
-----   -----
10922   73896 total
More or less –probably they added two or three more since you started reading.

Index

0220 count(where:)

0220 count(where:) implements exactly that.

[1, 2, 3, -1, -2]
    .count(where: { $0 > 0 }) // 3

0270 Add Collection Operations on Noncontiguous Elements

0270 adds RangeSet to refer to discontinuous ranges of elements.

var numbers = Array(1...15)
let indicesOfEvenNumbers: RangeSet<Int> = numbers.indices { $0.isMultiple(of: 2) }
let evenNumbers = numbers[indicesOfEvenNumbers]
  • RangeSet has set algebra methods (union, intersection, …) and a ranges variable returning its collection of ranges.
  • Collection and MutableCollection have methods to work with RangeSet.
  • New collection DiscontiguousSlice.

0301 Package Editor Commands

0301 adds commands to edit packages from the terminal. Example:

swift package add-dependency https://github.com/apple/swift-argument-parser
swift package add-target MyLibraryTests --type test --dependencies MyLibrary
swift package add-target MyExecutable --type executable --dependencies MyLibrary ArgumentParser
swift package add-product MyLibrary --targets MyLibrary

0405 String Initializers with Encoding Validation

0405 adds a new String failable initializer.

let validUTF8: [UInt8] = [67, 97, 0, 102, 195, 169]
let valid = String(validating: validUTF8, as: UTF8.self)

0408 Pack Iteration

A pack is a list of zero or more types or values.

  • Value parameter pack is a pack of values used as parameter.
  • Type parameter pack is a pack of types used as parameter, where each T may be a different type.

Syntax

<each T>, named Type Parameter Pack, is used as a generic type declaration.

  • T is called pack name
  • each T is called repetition pattern
  • each indicates T is a pack of types
  • T can optionally be constrained to a protocol (it would be <each T: Protocol>)

values: (repeat each T), named Value Parameter Pack is used to declare properties or function parameters.

  • var is the parameter name
  • repeat indicates multiple values
  • each T, called repetition pattern, refers to the types in the pack

repeat expression(each T), named Pack Expansion Expression, expands a type or value pack within expressions.

  • each T is called repetition pattern

After this word salad, let’s see an example to get an intuitive sense of what it all means.

Example

// the Type Parameter Pack indicates this type is generic over several types
struct Tuple<each T: Sendable> {
    // the Value Parameter Pack indicates this var is a pack of types
    var values: (repeat each T)
    
    // another Value Parameter Pack used as function argument
    mutating func set(values: (repeat each T)) {
        self.values = values
    }
    
    func printAndReturn<Value>(_ value: Value) -> Value {
        print("> \(value)")
        return value
    }
    
	// function is generic over a Type Parameter Pack, 
    func iterate<each U>(_ u: repeat each U) {
        // a Pack Expansion Expression is used to iterate over the types
        for value in repeat printAndReturn(each u) {
            print(". \(value)")
        }

        // another way to print each value
        repeat print(each u)
    }
}

var tuple = Tuple(values: (42, "Hello"))
tuple.set(values: (10, "Bye"))
tuple.iterate(1, "hello", true)

Example

Before SE-0408 I could pass arrays of different elements but only as a common supertype/protocol, and even then, I may still need the concrete type later. With type packs however, I can pass a ‘pack’ of different types.

// I replace this

// splitViewController.addSplitViewItem(
//     NSSplitViewItem(viewController: NSHostingController(rootView: LeftView()))
// )
// splitViewController.addSplitViewItem(
//     NSSplitViewItem(viewController: NSHostingController(rootView: RightView()))
// )
// splitViewController.addSplitViewItem(
//     NSSplitViewItem(viewController: NSHostingController(rootView: ThirdView()))
// )

// with this

splitViewController.addViews(LeftView(), RightView(), ThirdView())

extension NSSplitViewController {
    func addViews<each U: View>(_ u: repeat each U) {
        for view in repeat each u {
            addSplitViewItem(
                NSSplitViewItem(viewController: NSHostingController(rootView: view))
            )
        }
    }
}

Note that this wouldn’t work passing any View:

// Type 'any View' cannot conform to 'View'
// Only concrete types such as structs, enums and classes can conform to protocols
// Required by generic class 'NSHostingController' where 'Content' = 'any View'
NSSplitViewItem(viewController: NSHostingController(rootView: view))

Relevant links

0409 Access-level modifiers on import declarations

0409 enhances the import keyword so it can be prefixed with

  • public. Visible to all clients.
  • package. Visible inside modules of the same package (e.g. a library target and its test module).
  • internal. Visible inside this module.
  • fileprivate or private. Import is scoped to the source file declaring the import.

Example:

internal import DatabaseAdapter

// OK
internal func internalFunc() -> DatabaseAdapter.Entry {...}

// error: function cannot be public because its result uses an internal type
public func publicFunc() -> DatabaseAdapter.Entry {...} 

Upcoming feature flag: InternalImportsByDefault.

0410 Low-Level Atomic Operations

0410 is implemented in a new Synchronization framework that adds the following:

  • Lock-free types with well-defined semantics.
  • Explicit memory ordering arguments for atomic operations, which provide guarantees on visibility to other threads.
  • Basic operations (compare, load, store, add, etc.) for ints, pointers, floats, bool, and few other types. See UML.

Understanding this proposal requires knowing a few terms related to multi-threaded programming:

  • Visibility is the ability of a thread to see the effects of write memory operations performed by other threads.
  • Memory ordering are the rules for the order and visibility of memory operations (read and/or write operations) across multiple threads.
  • Release is an operation in where a thread marks its updates to shared memory as complete and available to other threads.
  • Acquire is an operation where a thread ensures it observes all prior modifications to shared memory that were released by other threads before proceeding.

These are the memory ordering arguments:

  • .relaxed: Guarantees that this operation is atomic.
  • .acquiring: Guarantees that all reads or writes to shared memory that occur after this operation will see the effects of previous releases on other threads. Used in read operations to ensure it sees the effect of previous writes.
  • .releasing: Guarantees that all reads and writes performed by the current thread are completed before the release. Used in write operations to guarantee that a subsequent acquire sees its effects.
  • .acquiringAndReleasing: Acquires and releases. Used by read-modify-write operations.
  • .sequentiallyConsistent: Guarantees that concurrent operations performed in a value of shared memory are seen as happening in the same order by all threads.

Example

Add one million ten times, with each million executing potentially from a different thread. GCD decides how many threads to run, up to but not exceeding 10.

import Synchronization
import Dispatch
let counter = Atomic<Int>(0)
DispatchQueue.concurrentPerform(iterations: 10) { _ in
  for _ in 0 ..< 1_000_000 {
    counter.wrappingAdd(1, ordering: .relaxed)
  }
}
print(counter.load(ordering: .relaxed))

0414 Region based Isolation

0414 enhances the compiler ability to spot cases where passing a non-sendable between boundaries is actually safe. This eliminates a few unnecessary compiler warnings.

For instance, actor A passes a UIImage to actor B, who returns a copy of the image but doesn’t change the original image. Now the compiler is able to see that there are no escaping references, changes, and that the original image is no longer used in its original context.

actor ImageProcessor {
    func scaleImageTo2x(_ image: UIImage) -> UIImage? {
		// UIImage is read without side effects
        let newSize = CGSize(width: image.size.width * 2, height: image.size.height * 2)
        let renderer = UIGraphicsImageRenderer(size: newSize)
        return renderer.image { _ in
            image.draw(in: CGRect(origin: .zero, size: newSize))
        }
    }
}

class ImageViewController: UIViewController {
    // UIImage is non sendable and crossing boundaries but no concurrent modification
	// to it is performed until returning from the ImageProcessor actor.
    @MainActor
    func updateImage() async {
        imageView.image = await ImageProcessor().processImage(UIImage(named: "example")!)!
    }
}

If this example is confusing to see it may be time to review a few basic concepts related to Sendable.

Sendable

Let’s review why we can’t share a non-Sendable between Tasks.
  • A Race Condition occurs when the behavior of software becomes dependent on the sequence or timing of uncontrollable events, such as the execution order of multiple threads. It typically happens when multiple threads access shared data and at least one thread modifies the data.
  • Mutual Exclusion is a mechanism that prevents several threads from concurrently accessing a shared value. That is, only one thread has access to state during a write.
  • A Task in Swift is a self-contained unit of execution designed to handle asynchronous operations. It executes code sequentially and can only run on one thread at a time, though it may switch threads after reaching suspension points—places in the code where execution can be paused and later resumed. Tasks are sometimes called boundaries of execution.
  • A Sendable value is one that can be passed between threads without violating thread-safety. A value is sendable if its state is immutable, or mutable but synchronized internally. If we send a non-Sendable value between boundaries (tasks), it may be modified in unpredictable ways, causing a race condition. For instance, imagine passing a counter where the read and increment operation is not atomic and is mixed with other read-and-increment operations.

0415 Function Body Macros

0415 adds an @attached(body) macro that implements functions or adds code to their existing implementation. It can be applied to functions, initializers, deinitializers, and accessors.

Example use cases:

  • Synthesizing remote procedure calls
  • Adding logging/tracing
  • Checking preconditions and invariants
  • Replacing function bodies with new implementations

For instance,

// this
@Remote
func stories(useId: Int) async throws -> [Story] {}

// could expand into this
@Remote
func stories(useId: Int) async throws -> [Story] {
    let request = Request<[Story]>(httpMethod: "GET", path: "/stories/id/\(useId)")
    return try await execute(request: request)
}

This feature is still experimental so it needs:

@_spi(ExperimentalLanguageFeature) import protocol SwiftSyntaxMacros

and also

swiftSettings: [
    .swiftLanguageVersion(.v6),
    .enableExperimentalFeature("BodyMacros")
]

0416 Subtyping for keypath literals as functions

0416 improves the use of key paths as functions.

For instance, in the example below (Derived) -> Base expects a result of type Base, but the keypath returns a subclass of Base. This is allowed for functions, and thanks to this proposal it is now also allowed for key paths.

class Base {
  var derived: Derived { Derived() }
}
class Derived: Base {}
let g1: (Derived) -> Base = \Base.derived

This is related to SE-0249, introduced in Swift 5.2. 0249 added the ability to use key paths as functions when the match was exact. This meant there were cases where we couldn’t replace functions with key paths, because functions accept contravariant (wider) parameters and may return covariant (narrower) results.

What is Contravariant and covariant?

class Animal {}
class Dog: Animal {}

func getAnimal() -> Animal {
    // Return types are covariant: 
    // they may return more specific types
    Dog()
}

func printAnimalSound(_ print: (Dog) -> Void) {
    print(Dog())
}
// Parameter types are contravariant: 
// they may accept more general types
printAnimalSound { (animal: Animal) in print("\(animal)") }

0417 Task Executor Preference

0417 adds API to specify a preferred executor for tasks and asynchronous functions. So the logic to choose an executor is as follows:

Executor preference API

await withTaskExecutorPreference(executor) { 
  await withDiscardingTaskGroup { group in 
    group.addTask {
      await nonisolatedAsyncFunc() // also runs on 'executor'
    }
  }
  async let number = nonisolatedAsyncFunc()
  await number
}

Task(executorPreference: executor) {
  await nonisolatedAsyncFunc()
}

Task.detached(executorPreference: executor) {
  await nonisolatedAsyncFunc()
}

await withDiscardingTaskGroup { group in 
  group.addTask(executorPreference: executor) { 
    await nonisolatedAsyncFunc()
  }
}

func nonisolatedAsyncFunc() async -> Int {
  return 42
}

actor Capybara { func eat() {} } 
let capy: Capybara = Capybara()
Task(executorPreference: executor) {
  // Execution is isolated to the 'capy' actor, 
  // but runs in the 'executor' TaskExecutor
  /// unless capy has a custom executor.
  try await capy.eat() 
}

What is an executor?

An executor is a component that manages the execution of tasks on available threads. Swift has three types of executors:
  • A default “global concurrent executor” for any work that don’t specify executor requirements. This is the case for actors, tasks, and asynchronous functions without a preference.
  • A SerialExecutor that executes tasks serially (one after another). This provides isolation for actors.
  • A TaskExecutor that executes tasks serially (one after another). This is just a source fo threads for execution.
SerialExecutor and TaskExecutor are defined as protocols, allowing for custom implementations.

Executor preference is inherited by

  • async let,
  • default actors (those without custom executors),
  • TaskGroup addTask() (and variants like ThrowingTaskGroup, etc.).

Executor preference is NOT inherited by

  • Task {}
  • Task.detached {}

Global variable globalConcurrentExecutor returns the global concurrent executor. Variable unownedTaskExecutor will unsafely return the executor you are in.

unownedTaskExecutor

struct MyEventLoopTaskExecutor: TaskExecutor {}

func test(expected eventLoop: MyEventLoopTaskExecutor) {
   withUnsafeCurrentTask { task in
       guard let task else {
           fatalError("Missing task?")
       }
       guard let current = task.unownedTaskExecutor else {
           fatalError("Expected to have task executor")
       }
        
       precondition(current == eventLoop.asUnownedTaskExecutor())
        
       // ...
   }
}
Example from 0417 > Inspecting task executor preference.

0418 Inferring Sendable for methods and key path literals

0418 improves how Swift handles Sendable conformance for methods and key path literals. Main takeaway: less compiler complaints. You’ll have to write less @Sendable annotations.

Enables automatic Sendability inference for certain methods and functions:

  • Unapplied and partially applied methods of Sendable types.
  • Non-local functions: These are functions defined at the global scope or as static methods, but not within other functions.
  • Example.

Sendability for functions and methods

struct User: Sendable {
  func updatePassword(new: String, old: String) -> Bool { ... }
}

let user = User()

// This is 'partially applied' (no arguments)
let partiallyApplied = user.updatePassword

// This is 'unapplied' (not associated with an instance)
let unapplied = User.updatePassword

Disables automatic Sendability inference for key path literals. Instead:

  • Key paths without non-Sendable captures will be inferred as Sendable when needed.
  • Developers can explicitly mark key paths as Sendable using & Sendable.
  • Example.

Sendability in key path literals

struct User: Sendable {
  var name: String
}
func takeSendableKeyPath(_ kp: KeyPath<User, String> & Sendable) {}

// \.name is Sendable because it doesn’t capture any non-Sendable
takeSendableKeyPath(\.name)

// this is not automatically Sendable unless you mark it
let namePath: KeyPath<User, String> & Sendable = \.name

Adds a compiler error for methods of non-Sendable types that have been marked @Sendable. Instead, you have to make the entire type Sendable. Example.

Methods of non-Sendable types can’t be marked @Sendable

class MyClass { // Not a Sendable
  var mutableState: Int = 0
  @Sendable func unsafeMethod() { // This will now be a compiler error
    mutableState += 1
  }
}
Note: this compiler should cause a compiler error with -enable-experimental-feature InferSendableFromCapture but doesn’t. Needs investigation.

0420 Inheritance of actor isolation

0420 lets functions opt-in to inherit the actor isolation of their caller. This is accomplished adding an isolated parameter with a default #isolation value. Example:

func accessCounter(isolation: isolated (any Actor)? = #isolation) async -> Int{
    if let actor = isolation as? MyActor {
        // 'actor.counter' can be accessed without suspension
        // because function inherited MyActor isolation
        return actor.counter
    } else {
        // does not inherit MyActor isolation
        return 0
    }
}

actor MyActor {
    var counter = 0
    func someMethod() async {
        // defaults to calling with this actor isolation (unless passing nil)
        let count = await accessCounter()
    }
}

The rules are:

  • If called from a non-isolated context, isolation will be nil.
  • If called from an actor method, isolation will be that actor.
  • If called from a context isolated to a global actor (like @MainActor), isolation will be the shared instance of that global actor (e.g. MainActor.shared).

Recap

So far an easy to understand proposal. However, so many language changes are starting to pile up. Let’s review function isolation and Sendability.

Function isolation in Swift refers to the context in which a function is allowed to execute and what data it can safely access. Currently there are:

  • Functions non-isolated. Callable from any context, can’t access actor state.
  • Functions isolated to a specific actor. Callable from that actor instance, can only access that actor isolated state.
  • Functions isolated to a global actor. This can be inferred by the use of an annotation or by context (such as in the methods of main-actor-isolated types).
  • Functions with custom isolation (SE-0420). Can inherit isolation from its caller or have dynamically determined isolation.
  • Example.

Function isolation

import Foundation

// 1. Non-isolated function
func nonIsolatedFunction() {
    print("This function can be called from anywhere")
}

// 2. Actor-isolated function
actor MyActor {
    func isolatedFunction() {
        print("This function is isolated to MyActor")
    }
}

// 3. Global actor-isolated function
@MainActor func mainActorFunction() {
    print("This function runs on the main actor")
}

// 4. Sendable function
@Sendable func sendableFunction() {
    print("This function is Sendable and can be called from any context")
}

// 5. Function with custom isolation (using SE-0420 proposal)
func customIsolated(isolation: isolated (any Actor)? = #isolation) async {
    print("This function inherits isolation from its caller")
}

// Usage examples:
func examples() async {
    // Non-isolated function can be called from anywhere
    nonIsolatedFunction()
    
    // Actor-isolated function must be called on an actor instance
    let myActor = MyActor()
    await myActor.isolatedFunction()
    
    // Global actor-isolated function
    await mainActorFunction()
    
    // Sendable function can be called from anywhere
    sendableFunction()
    
    // Custom isolated function
    await customIsolated() // Inherits caller's isolation
    await customIsolated(isolation: myActor) // Inherits MyActor isolation
}

Sendable functions, while not an isolation type, have restrictions on what these functions can capture and work with.

  • They can be called from any context, including across actor boundaries.
  • They must only capture Sendable data. This means:
    • Value types that are Sendable (structs, enums without associated references)
    • Classes marked as @Sendable or final class with only Sendable stored properties
    • Other Sendable functions
  • They cannot capture mutable state or non-Sendable references.
  • They don’t have special access to actor-isolated state.
  • When used as closure types, they enforce these rules at the point of closure creation.
  • Example.

Sendable functions

import Foundation

struct UserProfile: Sendable {
    let id: Int
    let name: String
}

// Sendable enum without associated references
enum UserStatus: Sendable {
    case active, inactive
}

// Sendable class (final class with only Sendable stored properties)
final class ImmutableConfig: Sendable {
    let apiKey: String
    init(apiKey: String) { self.apiKey = apiKey }
}

// Non-Sendable
class MutableData {
    var value: Int = 0
}

actor UserManager {
    private var users: [UserProfile] = []
    
    func addUser(_ user: UserProfile) {
        users.append(user)
    }
    
    // This function can be called across actor boundaries
    @Sendable func processSendableData(_ profile: UserProfile, _ status: UserStatus) -> String {
        return "Processed: \(profile.name) - Status: \(status)"
    }
}

@MainActor
func exampleUsage() async {
    let userManager = UserManager()
    let config = ImmutableConfig(apiKey: "12345")
    
    // Sendable closure capturing only Sendable data
    let sendableClosure: @Sendable () -> String = {
        // Capturing Sendable objects
        let profile = UserProfile(id: 1, name: "Alice")
        let status = UserStatus.active
        let apiKey = config.apiKey
        return "User: \(profile.name), Status: \(status), API Key: \(apiKey)"
    }
    
    Task.detached {
		// Sendable closures and functions can be called across actor boundaries
        print(await userManager.processSendableData(UserProfile(id: 2, name: "Bob"), .active))
        print(sendableClosure())
    }
    
    // Compiler error - cannot capture mutable state or non-Sendable references
    // let mutableData = MutableData()
    // let nonSendableClosure: @Sendable () -> Void = {
    //     mutableData.value += 1  // Error: Capturing mutable state
    // }
    
    // Compiler error - Sendable closures don't have special access to actor-isolated state
    // let invalidClosure: @Sendable () -> Void = {
    //     print(userManager.users)  // Error: Cannot access actor-isolated state
    // }
    
    // When used as closure types, Sendable rules are enforced at the point of creation
    func takeSendableClosure(@Sendable _ closure: @escaping () -> Void) {
        Task.detached { closure() }
    }
    
    // This compiles - closure only captures Sendable data
    takeSendableClosure {
        let _ = UserProfile(id: 3, name: "Charlie")
    }
    
    // Compiler error - closure captures non-Sendable data
    // let nonSendableData = MutableData()
    // takeSendableClosure {
    //     nonSendableData.value += 1
    // }
}

0421 Generalize effect polymorphism for AsyncSequence and AsyncIteratorProtocol

Changes in 0421:

  • Both protocols now have a Failure associated type that enable typed throws.
  • AsyncIteratorProtocol has a new next(isolation actor: isolated (any Actor)?) method –explained in 0420. To avoid breaking existing source with that new next() method, there is a default implementation in Swift 6.

As of 07/08/24 seems typed throws is not ready yet. See Where is FullTypedThrows?.

0422 Expression macro as caller-side default argument

Thanks to 0422, expression macros can now be used as caller-side default arguments for functions.

I’ll explain,

  • Default arguments for functions are usually evaluated in the function definition. Example: func line(line: Int = 0) {}.
  • However, using #line as argument passes the line of source code where the function was called. Example: func line(line: Int = #line) {}.
  • Thanks to this proposal expression macros are expanded with caller-side source location information and context. Macro authors can use context.location(of:) to access source location information.

0423 Dynamic actor isolation enforcement from non-strict-concurrency contexts

This proposal enhances the ability of the Swift compiler to detect concurrency issues in dependencies imported using @preconcurrency import. This feature primarily targets dependencies that haven’t implemented strict concurrency checking, and therefore, can’t be checked statically. Instead, the compiler will employ dynamic (= at runtime) checks on them.

As a developer, you don’t need to take any specific action. However, you may encounter additional runtime errors that highlight previously undetected data races. These errors actually improve your code’s safety by exposing concurrency issues that were previously silent.

This feature is gated behind the upcoming feature flag DynamicActorIsolation.

0424 Custom isolation checking for SerialExecutor

0424 introduces a new protocol requirement called checkIsolated() for SerialExecutor. This allows custom executors to provide their own logic for safety checks like assertIsolated and assumeIsolated.

Details

assertIsolated:

  • This is a runtime check that verifies whether the current execution context is isolated to a specific actor.
  • It’s typically used for debugging and testing purposes.
  • If the assertion fails (i.e., the code is not running in the expected isolated context), it triggers a runtime error.
  • It doesn’t affect release builds, as assertions are typically stripped out in those.

assumeIsolated

  • This is a more powerful variant that allows access to actor-isolated state.
  • It takes a closure as an argument and executes that closure with the assumption that the current context is isolated to the actor.
  • If the assumption is incorrect (i.e., the code is not actually isolated), it results in a runtime crash.
  • Unlike assertIsolated, it affects both debug and release builds.
  • It’s used when you need to access actor-isolated state in a context where the compiler can’t statically verify isolation.

Example.

Sendable functions

actor BankAccount {
    private var balance: Double = 0

    func deposit(_ amount: Double) {
        balance += amount
    }

    func withdraw(_ amount: Double) throws {
        guard balance >= amount else {
            throw NSError(domain: "InsufficientFunds", code: 1, userInfo: nil)
        }
        balance -= amount
    }

    nonisolated func riskyWithdrawal(_ amount: Double) throws {
        // This would normally be a compiler error because we're accessing actor-isolated state
        // from a nonisolated context. But we can use assertIsolated and assumeIsolated to 
        // perform the operation if we're certain about the execution context.

        assertIsolated(self, "This method must be called while isolated on the BankAccount actor")

        try assumeIsolated {
            try self.withdraw(amount)
        }

        print("Withdrawal successful")
    }
}

// Usage:
let account = BankAccount()

Task {
    await account.deposit(100)
    
    do {
        try await account.riskyWithdrawal(50)
    } catch {
        print("Withdrawal failed: \(error)")
    }
}

// This would crash because it's not isolated to the actor
// try account.riskyWithdrawal(50)

0426 BitwiseCopyable

0426 adds a BitwiseCopyable marker protocol for types that are “bitwise-copyable”. This means they can be safely copied or moved using direct memory operations like memcpy. Many basic Swift types (like integers, floating-point numbers, pointers) will conform to this protocol.

  • If a developer conforms a type to BitwiseCopyable, the compiler checks if it qualifies.
  • Non exported struct and enum are inferred BitwiseCopyable if all stored members are. If the enum is @frozen BitwiseCopyable conformance will be inferred even if they are exported (public).
  • Developers can explicitly opt-out using ~BitwiseCopyable in the type’s inheritance list.

Example:

// opting out an otherwise BitwiseCopyable type
// because you plan to add non BitwiseCopyable stuff
struct Coordinate4: ~BitwiseCopyable {
    var x, y, z, w: Double
}

As of 07/08/2024 I don’t see BitwiseCopyable in the online docs but it’s on Xcode 16 beta 2.

0428 Resolve DistributedActor protocols

Sorry, I have zero experience with IPC and distributed actors so this is the one proposal I skipped.

0430 sending parameter and result values

0430 adds a sending keyword to indicate that a parameter or result value can be safely sent across isolation boundaries or merged into actor-isolated regions. This extends the concept of region isolation introduced in SE-0414, allowing non-Sendable typed values to be safely sent over isolation boundaries. Confused so far?

Fancy speaking

In the context of this proposal, there are three regions to consider, each with its scope of execution and local state: synchronous code, tasks, and actors.

Tasks and actors are isolated in the sense that their local state can’t be freely accessed:
  • A task doesn’t have properties or methods that can be accessed but can capture values when defined or send values explicitly starting other tasks or calling methods.
  • An actor manages its own state with by queuing up calls to prevent concurrent execution.
Therefore,
  • An isolated region is the scope of a task or actor.
  • Crossing an isolation boundary is sending a value to or from a region, which is any combination of sync code/task/actor to sync code/task/actor (except sync code to sync code which is non isolated).
  • Merging a value into an actor-isolated region is storing that value in the actor.

Requirements:

  • sending parameters require the argument to be in a disconnected region at the call site. Meaning, when you call the function passing this parameter, there are no other references to this parameter that could lead to concurrent modifications. If I create the value just to pass it to you, no one else can touch it.
  • sending results require the function to return a value in a disconnected region. Meaning, the value should be freshly created within the function or otherwise detached from any shared state that could lead to concurrent access.

This example sends and return a non referenced value, therefore safe. The compiler would have emitted an error if the value was stored.

func process(_ resource: sending NonSendableResource) -> sending NonSendableResource {
    NonSendableResource(data: resource.data * 10) // created to return it
}
process(NonSendableResource(data: 10)) // created to pass it

sending can be combined with inout and ownership modifiers like consuming and borrowing.

0431 @isolated(any) Function Types

0431 introduces a new @isolated(any) attribute for function types in Swift. A function value with this type dynamically (at runtime) carries the isolation of the function that was used to initialize it.

Example:

func traverse(operation: @isolated(any) (Node) -> ()) {
    // the isolation can be read using the special isolation: (any Actor)? property
    let isolation = operation.isolation
}
  • If the function is dynamically non-isolated, the value of isolation is nil.
  • If the function is dynamically isolated to a global actor type G, the value of isolation is G.shared.
  • If the function is dynamically isolated to a specific actor reference, the value of isolation is that actor reference.

When called from an arbitrary context, these functions are assumed to cross an isolation boundary, requiring await. Example:

@ModelActor
class PersistenceManager {
    // New method using @isolated(any)
    func performTransaction<T>(_ operation: @isolated(any) () throws -> T) async throws -> T {
        if operation.isolation === ModelActor.shared {
            // If the operation is already @ModelActor-isolated, we can call it directly
            result = try operation()
        } else {
            // Otherwise, we need to await it
            result = try await operation()
        }
    }
    // ...
}

0432 Borrowing and consuming pattern matching for noncopyable types

0432 is available with experimental feature flag BorrowingSwitch.

Before this proposal, pattern matching a non copyable value will always consume it. Now it depends:

  • If the value passed is a temporary value it’s always consumed.
  • If the value passed is stored it will be consumed only if passed to a consuming argument.

Example:

enum FileHandle: ~Copyable {
    case file(path: String)
    case network(url: URL)
}

// this parameter is not stored so it will always be consumed
processHandle(FileHandle.network(url: url))

// this parameter is stored, it may be borrowed or consumed
let handle = FileHandle.network(url: url)
processHandle(handle)

The argument is consumed when marked as such. Example:

// marked as consuming
func processHandle(_ handle: consuming FileHandle) {
    switch handle {  // This line consumes 'handle'
    // ...
    }
    // 'handle' is no longer available here
}

// not marked as consuming
func processHandle(_ handle: FileHandle) {
    switch handle {  // This line borrows 'handle'
    // ...
    }
    // 'handle' is still available for more pattern matching
}

This applies to pattern matching performed with switch, if, while, for, and guard.

0434 Usability of global-actor-isolated types

0434 lifts unnecessary restrictions. Below, I quote in bold the proposal changes.

Stored properties of Sendable type in a global-actor-isolated value type are implicitly nonisolated, and can be optionally declared as nonisolated without using (unsafe) to be treated as such outside the defining module. This is because all global actors serialize access to their members, and because the value is safe to send across boundaries. The benefit is that synchronous code can access the members while still respecting the thread-safety guarantees.

Note that wrapped properties and lazy-initialized properties with Sendable type still must be isolated because they are computed properties, not stored values.

import Foundation

struct UserProfile: Sendable {
    var name: String
}

@MainActor
class UserAccount {
    nonisolated var profile: UserProfile // Sendable property
    
    init(profile: UserProfile) {
        self.profile = profile
    }
}

let user = UserAccount(profile: UserProfile(name: "John Doe", age: 28))
print("User: \(user.profile.name)") // access from a synchronous context

@Sendable is inferred for global-actor-isolated functions and closures. Example:

// 'globallyIsolated' can only run serially on the main actor, 
// so it’s safe to assume it is Sendable
func test(globallyIsolated: @escaping @MainActor () -> Void) {
  Task {
    await globallyIsolated() //okay
  }
}

Global-actor-isolated closures are allowed to capture non-Sendable values despite being @Sendable. The above code is data-race safe, since a globally-isolated closure will never operate on the same instance of NonSendable concurrently. Example:

class NonSendable {}

func test() {
    let ns = NonSendable()

    let closure { @MainActor in
        print(ns)
    }

    Task {
        await closure() // okay
    }
}

Permit global-actor-isolated subclasses of non-isolated, non-Sendable classes, but prevent them from being Sendable. Subclasses may add global actor isolation when inheriting from a nonisolated, non-Sendable superclass. In this case, an implicit conformance to Sendable will not be added, and explicitly specifying a Sendable conformance is an error. This is because an overridden method does not inherit the isolation of its type, instead, it preserves the isolation of the superclass so it can be usable anywhere the superclass is usable.

This proposal is gated behind upcoming feature flag GlobalActorIsolatedTypesUsability.

0435 Swift Language Version Per Target

0435: SPM Targets can specify a language version to facilitate migration.

.target(
    name: "Persistence", 
    swiftSettings: [.swiftLanguageVersion(.v6)]
)

Conclusion

Recap of the recap: how the language has evolved?

  • Fine grained control in memory (sending) and performance (atomics and preferred executors).
  • Powerful metaprogramming (side caller context on macros) and generics (value and type packs).
  • The usual ergonomic changes like count(where:) (0220), string initializers (0405), improvements to key paths as functions.

and finally the big one

  • Safer code with less warnings. 9 out of 23 proposals enhance the compiler reasoning about isolation contexts, sendable inference, and @preconcurrency code. For instance, now we can pass non-sendables for read-only purposes without irritating warnings.

Concurrency proposals

  • 0414 Region based isolation
  • 0417 Task executor preference
  • 0418 Inferring sendable for methods and key path literals
  • 0420 Inheritance of actor isolation
  • 0423 Dynamic actor isolation enforcement from non-strict-concurrency contexts
  • 0424 Custom isolation checking for serialexecutor
  • 0430 `sending` parameter and result values
  • 0431 `@isolated(any)` function types
  • 0434 Usability of global-actor-isolated types

Reading this took some time and effort. But the main benefit is exactly that exercise, reasoning to polish my mental model. Despite some new keywords I think the language got easier. Overall I’m glad for all these changes.

For those skipping the reading, Swift will silently work better. Type packs is the one that’s going to confuse people who first see it, but you get the intuition right with just one example. Similar with sending, @isolated, and preferred executors. I think we are all better with Swift 6.