WWDC Quick Look 💓 By SwiftGGTeam
What's new in Swift

What's new in Swift

Watch original video

Highlight

Swift 6.3 and 6.4 improve everyday coding ergonomics (anyAppleOS availability, module selectors, @diagnose warning control), expand the standard library (task cancellation shields, Dictionary.mapKeyedValues, FilePath), add two-way interoperability between Swift Testing and XCTest, broaden cross-platform interoperability (@C export to C, Swift-Java, Wasm), and significantly extend the ownership system (Iterable, borrow/mutate accessors, Ref/MutableRef) so high-performance code is easier to write without giving up safety.

Core Ideas

The small annoyances in daily coding are disappearing

Some details in Swift have always required a few extra keystrokes. For example, when some or any was combined with an optional type, you had to add parentheses: (any Protocol)?. Swift 6.4 removes that rule, and the compiler now understands your intent automatically. (00:52)

When an error thrown from a concurrent task was silently ignored, older compilers gave no hint. The compiler now emits a warning that tells you to either catch the error inside the task or store the task and inspect it later. (01:12) The old limitation on calling async functions inside defer blocks has also been lifted. (01:22)

Sendable checking has improved too. A class that previously needed @unchecked Sendable because it contained a weak var can now use weak let instead; immutability lets it pass Sendable checking. (01:27) If a type explicitly should not support Sendable, you can declare that with the new ~Sendable syntax, while subclasses can still independently declare Sendable conformance. (01:38)

Say goodbye to verbose platform availability annotations

Apple’s ecosystem has grown from two platforms to five, and availability annotations grew with it. Previously, a new API had to list five platforms:

@available(macOS 27, iOS 27, watchOS 27, tvOS 27, visionOS 27, *)
func showStatus() { ... }

Swift 6.4 introduces anyAppleOS, reducing that to one line: (02:17)

@available(anyAppleOS 27, *)
func showStatus() { ... }

@available(anyAppleOS 27, *)
@available(tvOS, unavailable)
func launch() { ... }

#if os(anyAppleOS)
func makeLiveActivityWidget() -> some Widget { ... }
#endif

When a platform is an exception, use anyAppleOS for the default and then override it with a specific platform attribute. #if os(anyAppleOS) also works for conditional compilation.

Precisely control compiler warnings

During API evolution, old APIs are marked deprecated, but sometimes you cannot migrate immediately. The @diagnose attribute gives fine-grained control over warnings on a specific declaration: (02:40)

@diagnose(DeprecatedDeclaration, as: ignored, reason: "Flying with surplus hardware")
func makeApolloSoyuzMission() -> Mission { ... }

@diagnose(StrictMemorySafety, as: warning)
func uplinkCommand(from receiver: inout Receiver, to computer: inout Computer) { ... }

@diagnose(ErrorInFutureSwiftVersion, as: error)
func fetchPosition() -> (x: Double, y: Double, z: Double) { ... }

Key points:

  • as: ignored disables a specific warning without affecting the rest of the project
  • as: warning enables warning groups that are off by default, such as StrictMemorySafety
  • as: error promotes warnings that will become errors in a future Swift version into errors today

Resolve module name conflicts

When two modules export types with the same name, ModuleName.TypeName can fail if the module itself also contains a type with the module’s name. Swift resolves it as a type name first, not a module name. Swift 6.3 introduces the module selector ::, whose left side is always a module name: (03:47)

import Rocket
import GiftShopToys

let rocket1 = SaturnV()            // Ambiguous error
let rocket2 = Rocket.SaturnV()     // If the Rocket module also has a Rocket type, this can resolve incorrectly
let rocket3 = Rocket::SaturnV()    // Correctly points to SaturnV in the Rocket module

The module selector also works for method-name conflicts: (05:00)

launchPadTechnician.HumanResources::fire()

Use it defensively in macro expansions and generated code, because you do not know what modules the target project imports.

Details

New standard library tools

Task cancellation shields

Checking task cancellation matters, but some operations must finish even if the task has been cancelled, such as completing a disk write to avoid file corruption. withTaskCancellationShield makes cancellation checks inside the shielded region always return false: (06:40)

extension EmergencyTransponder {
    func sendSOS() {
        withTaskCancellationShield {
            radio.send(makeSOSPacket())
        }
    }
}

Key points:

  • Task.isCancelled is always false inside the shielded region
  • Keep the region short and focused on completing or rolling back work that has already started

Dictionary.mapKeyedValues

mapValues receives only the old value. If you need the key to compute the new value, you previously had to construct the dictionary manually. mapKeyedValues now gives the closure both key and value: (07:06)

func makeCalendarDisplayNames(for missions: [Mission: LaunchWindow]) -> [Mission: String] {
    missions.mapKeyedValues { mission, launchWindow in
        makeDisplayName(for: mission, in: launchWindow)
    }
}

FilePath

Different platforms represent file paths with subtle differences. The Swift standard library adds a FilePath type based on Swift System, correctly handling macOS named resources: (07:14)

var path: FilePath = "/var/www/static"
path.components.append("WWDC")
print(path.components)
// ["var", "www", "static", "WWDC"]

var path2: FilePath = "/var/www/static/..namedresource/rsrc"
print(path2.components)
// ["var", "www", "static"]

Swift Testing improvements

Issue severity and dynamic cancellation

Tests can record non-fatal issues without blocking CI: (07:41)

@Test(arguments: allRockets)
func testBurn(rocket: Rocket) throws {
    rocket.burn(for: .seconds(150))
    let remaining = rocket.propellantKg / rocket.totalPropellantKg

    if remaining < 0.10 {
        Issue.record(
            "\(rocket.name) remaining fuel is below 10% reserve target",
            severity: .warning
        )
    }

    #expect(remaining > 0.02, "\(rocket.name) propellant critically low - abort")
}

Parameterized tests can dynamically skip unsuitable arguments: (07:52)

@Test(arguments: allRockets)
func testBurn(rocket: Rocket) throws {
    if rocket.engineType == .solid {
        try Test.cancel("\(rocket.name) has solid fuel")
    }
    // ...
}

Two-way XCTest interoperability

In Swift 6.4, failures from XCTest assertions called inside Swift Testing are reported as test issues instead of immediately crashing. Conversely, calling #expect inside an XCTestCase also works normally: (08:34)

// Call XCTest from Swift Testing
func checkedTransmitAndReceive(on radio: Radio,
                               packet: Packet,
                               expectedByteCount: Int) throws -> [UInt8] {
    try radio.transmit(bytes: packet.data)
    let bytes = try radio.receive()
    XCTAssertEqual(bytes.count, expectedByteCount)
    return bytes
}

@Test
func pingTest() throws {
    let radio = Radio()
    let bytes = try checkedTransmitAndReceive(on: radio, packet: .ping, expectedByteCount: 8)
    #expect(bytes == [0x00, 0x00, 0xf0, 0x37, 0x0f, 0xc7, 0x00, 0x01])
}
// Call Swift Testing from XCTest
class RadioTests: XCTestCase {
    func testPingPacketTransmission() {
        let radio = Radio()
        let bytes = try checkedTransmitAndReceive(on: radio,
                                                  packet: .ping,
                                                  expectedByteCount: 8)
        #expect(bytes == [0x00, 0x00, 0xf0, 0x36, 0x0f, 0xc7, 0x00, 0x02])
    }
}

The default report level is warning, and it can be upgraded to test failure in Xcode build settings.

Subprocess 1.0

The Subprocess package has officially reached 1.0. The API is simpler, and cross-platform support is much stronger. Output streams are accessed through AsyncBufferSequence, ensuring each stream is created only once: (10:01)

let result = try await Subprocess.run(.name("ls"),
                                      input: .none,
                                      output: .sequence,
                                      error: .string(limit: 4096)) { execution in
    execution.standardOutput.strings().filter { $0.hasSuffix(".obj") }
}

for try await objectFile in result.closureOutput {
    print("Object file: \(objectFile)")
}

Key points:

  • strings() reads output line by line and correctly handles grapheme cluster boundaries
  • Platform-specific process file descriptors and termination statuses are supported

Foundation: ProgressManager

ProgressManager is a new Foundation progress-reporting type designed for the async/await concurrency model. It separates progress composition from progress reporting and supports type-safe metadata attachment: (10:37)

let manager = ProgressManager(totalCount: 100)
try await rocket.launch(mission.subprogress(assigningCount: 100))

// Observe progress
Task {
    for await update in Observations({ mission.fractionCompleted }) {
        print("Mission \(Int(update * 100))%")
    }
}

// Attach metadata
extension Rocket {
    func ascend(_ progress: consuming Subprogress) async throws {
        let stage = progress.start(totalCount: 3)
        stage.deltaV = 3_400; try await burn(); stage.complete(count: 1)
        stage.deltaV = 2_100; try await stageSeparation(); stage.complete(count: 1)
        stage.deltaV = 1_800; try await coast(); stage.complete(count: 1)
    }
}

print("Δv to orbit: \(mission.summary(of: \.deltaV)) m/s")

Cross-platform interoperability

Export Swift functions to C

Swift 6.4 can expose Swift functions to C with the @C attribute, similar to @objc but aimed at C interoperability: (12:39)

@c
@implementation
func launchWindowLength(_ window: Span<LaunchWindow>) -> TimeInterval {
    window[0].end.timeIntervalSince(window[0].start)
}

Key points:

  • Use @c and @implementation together to implement functions declared in a C header
  • Swift Span automatically bridges to C arrays
  • The compiler prevents passing C-incompatible types

Swift-Java interoperability

The Swift-Java package now supports calling Swift async and throwing functions from Java. The generic system supports constrained extensions and Java classes conforming to Swift protocols. The official Swift SDK for Android is now available from swift.org. (15:14)

WebAssembly support

The open-source Swift toolchain can now compile to WebAssembly. The JavaScriptKit project makes bridging between Swift and JavaScript safer and faster. The Goodnotes team found that safe bridging was 35-40 times faster than dynamic bridging. (16:40)

Embedded Swift expansion

Embedded Swift adds support for existential types and untyped throws. Debugging metadata is stored in DWARF instead of the binary itself, greatly reducing size while improving core-dump debugging. (18:08)

Performance tuning: control optimizer decisions

Inlining control

Swift 6.4 adds @inline(always), paired with the existing @inline(never): (21:30)

@inline(never)
func makeInts(randomized: Bool) -> [256 of Int] { ... }

@inline(always)
func makeInts(randomized: Bool) -> [256 of Int] { ... }

Key points:

  • @inline(never) forbids inlining and is useful when code size matters
  • @inline(always) forces inlining and works best with final class methods
  • Virtual method calls may still be impossible to inline

Generic specialization

The @specialized attribute lets library authors tell the compiler to generate specialized versions for particular types: (22:17)

@specialized(where Values == [UInt8])
func histogram<Values>(of values: Values) -> [256 of Int] where Values: Sequence<UInt8> {
    var result = makeInts(randomized: false)
    for value in values {
        result[Int(value)] += 1
    }
    return result
}

Key points:

  • Write a where clause in the attribute to constrain generic parameters
  • The compiler generates specialized versions for matching constraints and removes generic overhead
  • This is useful for generic functions frequently called with specific types

Ownership system expansion

Iterable protocol

In Swift 6.4, for loops support the new Iterable protocol. Unlike Sequence, Iterable accesses elements through borrowing, avoiding data copies and reference counting: (26:18)

protocol Iterable<Element, Failure>: ~Copyable, ~Escapable {
    associatedtype Element: ~Copyable
    associatedtype IterableIterator: IterableIteratorProtocol<Element, Failure>, ~Copyable, ~Escapable
    associatedtype Failure: Error = Never

    func makeIterableIterator() -> IterableIterator
    var underestimatedCount: Int { get }
}

Key points:

  • for loops prefer Sequence and fall back to Iterable
  • Non-copyable elements are supported
  • Typed throws are supported
  • Mutating the Iterable during iteration is forbidden and enforced by exclusivity checking
  • Elements are returned in batches through Span, improving efficiency

borrow and mutate accessors

get/set accessors work by copying data. For large value types, such as an InlineArray of 256 Int values at about 2 KB, modifying one element copies the whole structure twice. The new borrow and mutate accessors operate directly on the original storage: (28:19)

@safe public struct UniqueBox<Value: ~Copyable>: ~Copyable {
    private let valuePointer: UnsafeMutablePointer<Value>

    public init(_ value: consuming Value) { ... }

    public var value: Value {
        borrow { valuePointer.pointee }
        mutate { &valuePointer.pointee }
    }
}

Key points:

  • borrow provides read-only shared access without copying
  • mutate provides exclusive write access and mutates in place
  • Non-copyable value types are supported
  • UniqueBox has joined the standard library

Ref and MutableRef

Ref is similar to Span, but for a single value. It can be stored in a variable, passed to functions, and returned from functions: (29:42)

func updateCount<Key: Hashable>(
    for key: Key,
    from sets: [Set<Key>],
    in counts: inout [Key: Int]
) {
    var countRef = MutableRef(&counts[key, default: 0])

    for set in sets {
        if set.contains(key) {
            countRef.value += 1
        }
    }
}

Key points:

  • MutableRef(&expression) creates a reference from write access using the prefix &
  • Repeated dictionary lookups can be moved outside the loop
  • Ref is non-escapable, so the compiler knows the access ends when the variable leaves scope
  • This avoids older, obscure tricks that required inout parameters plus nested functions

Other new types:

  • UniqueArray: a non-copyable dynamic array with no reference-counting overhead
  • withTemporaryAllocation: replaces UnsafeMutableBufferPointer with OutputSpan
  • Continuation: compile-time checking that resume is called exactly once, combining the safety of CheckedContinuation with the efficiency of UnsafeContinuation

Key Takeaways

1. Use anyAppleOS to simplify multi-platform code

What to do: Replace all five-platform @available annotations and #if os conditional-compilation checks in your project with anyAppleOS.

Why it is worth doing: It reduces boilerplate and lowers the maintenance cost of adding new platforms.

How to start: Search for patterns like @available(macOS and #if os(macOS) || os(iOS) and replace them in batches.

2. Use @diagnose to manage technical debt gradually

What to do: Add @diagnose(DeprecatedDeclaration, as: ignored) to deprecated API call sites that cannot be migrated yet, instead of disabling warnings globally.

Why it is worth doing: It precisely limits warning suppression and avoids missing deprecated calls that truly need attention.

How to start: Filter deprecated warnings in Xcode’s Issue Navigator and evaluate each call site before adding @diagnose.

3. Use Iterable to optimize large-data traversal

What to do: Implement Iterable for custom collection types instead of Sequence.

Why it is worth doing: It avoids copying and reference-counting overhead when traversing large collections and supports non-copyable elements.

How to start: Define the iterator returned by makeIterableIterator() and implement nextSpan(maximumCount:) to return Span<Element> batches.

4. Use borrow/mutate accessors to optimize large value-type properties

What to do: Replace get/set with borrow/mutate on containers that wrap large value types, such as caches or configuration stores.

Why it is worth doing: It eliminates implicit copies during large value-type property access and operates directly on underlying storage.

How to start: Replace accessor keywords in computed properties on custom types, using UniqueBox or your own pointer management where appropriate.

5. Use MutableRef to remove repeated dictionary lookups

What to do: When a loop repeatedly modifies the same dictionary key, use MutableRef to hoist the lookup out of the loop.

Why it is worth doing: It reduces O(n) repeated hash lookups and improves hot-path performance.

How to start: Find code that repeatedly accesses dict[key] inside a loop, extract var ref = MutableRef(&dict[key, default: 0]), and use ref.value inside the loop.

  • 267 - Swift Testing — A deeper look at Swift Testing migration strategies and advanced usage
  • 269 - SwiftUI — How SwiftUI’s new features work with Swift language improvements
  • 328 - MLX Swift — Practical use of Swift’s ownership system in high-performance computing
  • 258 - Xcode 27 — New Xcode 27 features and Swift development workflow improvements
  • 289 - AppKit modernization — Applying new Swift language features in AppKit modernization

Comments

GitHub Issues · utterances