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

What’s new in Swift

Watch original video

Highlight

Swift 6.2 shifts the concurrency model from “concurrent by default” to “single-threaded by default”, adds standard library types like InlineArray, Span, Subprocess, and Observations, and lets macro projects cut clean build time sharply by using a pre-built swift-syntax.


Core content

Anyone who has written Swift 6 concurrency code for a while has hit the same kind of error. Add an async method to a plain class and the compiler warns that it crosses the main actor boundary. Make a @MainActor type conform to a regular protocol and the compiler says the conformance may run off-thread. The most common shapes in an app — UI types, singletons, callbacks — all get cut by the concurrency safety check. Developers either sprinkle @MainActor everywhere or rewrite their code into something they no longer fully understand.

Swift 6.2 rethinks this. The new default: an async function runs on the actor of its caller; @MainActor can be turned on as the module-wide default inference mode; conformances can be marked @MainActor Exportable so a main-actor type can legally conform to a protocol. An app that runs entirely on the main thread reads like Swift 5 again, but still keeps the Swift 6 data-race checks. When a CPU-heavy task does need to run off the main thread, mark it once with @concurrent — the intent is clear and the performance is predictable.

Beyond concurrency, 6.2 also reworks many everyday APIs: NotificationCenter gives notifications concrete types and removes the dictionary lookups and force casts; the new Subprocess package launches a child process in one line of await run(.name("pwd")); Observations wraps @Observable state changes into an AsyncSequence; Swift Testing adds Attachment and exit tests. On the toolchain side, swiftly 1.0 ships on macOS, a pre-built swift-syntax shaves minutes off clean builds for macro projects, and Explicitly Built Modules removes the long pause on the first p/po in LLDB.


Details

The new Subprocess package (09:44). The Foundation Workgroup pulls “spawn a child process”, the most common need in scripts, into its own package:

import Subprocess

let swiftPath = FilePath("/usr/bin/swift")
let result = try await run(
  .path(swiftPath),
  arguments: ["--version"]
)

let swiftVersion = result.standardOutput

Key points:

  • import Subprocess: comes from the standalone swift-subprocess package, currently at 0.1, gathering feedback before 1.0.
  • .path(swiftPath): a FilePath gives the full path to the executable; pass a string and it is looked up via $PATH.
  • try await run(...): the returned result carries exit status, standardOutput, standardError, and so on, fitting naturally into the async/await model.

NotificationCenter gets concrete types (11:34). Observing keyboard notifications used to mean passing the UIResponder.keyboardWillShowNotification string, digging a CGRect out of the userInfo dictionary, and dealing with main actor isolation. The new API folds all of that into one line:

import UIKit

@MainActor
class KeyboardObserver {
  func registerObserver(screen: UIScreen) {
    let center = NotificationCenter.default
    let token = center.addObserver(
      of: screen,
      for: .keyboardWillShow
    ) { keyboardState in
      let startFrame = keyboardState.startFrame
      let endFrame = keyboardState.endFrame

      self.keyboardWillShow(startFrame: startFrame, endFrame: endFrame)
    }
  }

  func keyboardWillShow(startFrame: CGRect, endFrame: CGRect) {}
}

Key points:

  • for: .keyboardWillShow: the compiler checks at compile time that UIScreen actually posts this notification; misspell the name and the build fails.
  • keyboardState.startFrame: the payload is a strongly typed struct, no more dictionary key plus as? CGRect.
  • The notification is declared as NotificationCenter.MainActorMessage, so the callback already runs on the main actor; no extra MainActor.assumeIsolated is needed.

Isolated conformances fix the cross-boundary protocol problem (33:04). A @MainActor type conforming to a plain protocol used to fail with “crosses into main actor-isolated code”. Swift 6.2 lets you write an actor annotation on the conformance:

protocol Exportable {
  func export()
}

extension StickerModel: @MainActor Exportable {
  func export() {
    photoProcessor.exportAsPNG()
  }
}

Key points:

  • @MainActor Exportable: declares that this conformance can only be used in main-actor context.
  • The caller must itself run on the main actor, otherwise the compiler errors out. This relaxes the cross-boundary check from “conformance forbidden” to “checked at the use site”.
  • Combined with the @MainActor module-wide default inference mode, single-threaded apps almost never need to write @MainActor annotations by hand.

Explicitly offload to the background (35:06). Under the new model, async runs on the caller’s actor by default; switching to the global concurrent executor takes an explicit annotation:

class PhotoProcessor {
  var cachedStickers: [String: Sticker]

  func extractSticker(data: Data, with id: String) async -> Sticker {
      if let sticker = cachedStickers[id] {
        return sticker
      }

      let sticker = await Self.extractSubject(from: data)
      cachedStickers[id] = sticker
      return sticker
  }

  @concurrent
  static func extractSubject(from data: Data) async -> Sticker {}
}

Key points:

  • extractSticker has no @concurrent, so it runs on the caller’s actor; cache reads and writes are thread-safe by construction.
  • @concurrent static func extractSubject: marked as runnable concurrently, leaves the main actor and runs on the global executor.
  • The hop for CPU-heavy work happens in one place. When tuning performance, you only need to look at the few @concurrent functions.

Observations turns state changes into an AsyncSequence (14:05):

let player = Player(name: "Holly")
let values = Observations {
  let score = "\(player.score) points"
  let item =
    switch player.item {
    case .none: "no item"
    case .banana: "a banana"
    case .star: "a star"
    }
  return "\(score) and \(item)"
}

player.score += 2
player.item = .banana

for await value in values { print(value) }

Key points:

  • Observations { ... }: every @Observable property read inside the closure is tracked automatically.
  • Multiple writes within the same tick (score += 2 and item = .banana) are coalesced into one transactional update, so consumers never see an intermediate state.
  • for await value in values: drives UI state as an AsyncSequence, plugging into async loops outside SwiftUI.

Takeaways

1. Turn on @MainActor default inference mode in Xcode build settings

Why it pays off: single-threaded UI apps are the shape of most product code. Turning this on removes screens full of @MainActor annotations, brings the code closer to Swift 5 readability, and still keeps Swift 6 data-race checking.

How to start: flip it on once via the SwiftSettings API in the SwiftPM manifest instead of editing files one by one; new projects get it by default.

2. Replace Array with InlineArray on performance-critical paths

Why it pays off: fixed-size data (color matrices, lookup tables, grid state) stored in an InlineArray lives inline on the stack, with no heap allocation or reference counting. Rendering, decoding, and signal-processing paths often see clear gains.

How to start: use Instruments to find allocation hot spots on a hot path, then replace them one at a time. Pair it with Span for safe zero-copy views.

3. Migrate NotificationCenter observers to the concrete-type API

Why it pays off: it removes three classes of common bugs — typo in the notification name, typo in a userInfo key, and crossing the main actor boundary. Call sites are half the size, and the type is the documentation.

How to start: begin with notifications Apple has already migrated, such as UIKit keyboard and app lifecycle events. For custom notifications, use the MainActorMessage / AsyncMessage protocols with a concrete payload type.

4. Wire pre-built swift-syntax into your macro project

Why it pays off: each clean build on CI saves several minutes, and the first local Xcode build is noticeably faster. Once the macro author publishes a tagged release, downstream users get the win without any change.

How to start: move to Xcode 26 / Swift 6.2, lock the macro package’s swift-syntax dependency to a tagged release, and check the build log to confirm the pre-built artifacts are picked up.

5. Replace scattered Task.detached with @concurrent

Why it pays off: putting “this work leaves the current actor” into the function signature instead of the call site makes the code easier to review and gives performance analysis a clear boundary.

How to start: audit existing Task.detached and DispatchQueue.global().async calls. Anything that can be expressed as a static function becomes @concurrent; keep runtime dispatch only for the few cases that need it.


Comments

GitHub Issues · utterances