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, andObservations, 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): aFilePathgives the full path to the executable; pass a string and it is looked up via$PATH.try await run(...): the returnedresultcarries 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 thatUIScreenactually posts this notification; misspell the name and the build fails.keyboardState.startFrame: the payload is a strongly typed struct, no more dictionary key plusas? CGRect.- The notification is declared as
NotificationCenter.MainActorMessage, so the callback already runs on the main actor; no extraMainActor.assumeIsolatedis 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
@MainActormodule-wide default inference mode, single-threaded apps almost never need to write@MainActorannotations 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:
extractStickerhas 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
@concurrentfunctions.
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@Observableproperty read inside the closure is tracked automatically.- Multiple writes within the same tick (
score += 2anditem = .banana) are coalesced into one transactional update, so consumers never see an intermediate state. for await value in values: drives UI state as anAsyncSequence, 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.
Related sessions
- Code-along: Elevate an app with Swift concurrency — follow along in code to upgrade an existing app to the Swift 6.2 concurrency model
- Embracing Swift concurrency — a full walkthrough of single-threaded by default, isolated conformances, and the rest of the design
- Explore Swift performance — the underlying mechanics of InlineArray, Span, and memory safety
- What’s new in Xcode 26 — Explicitly Built Modules, debugger improvements, and other toolchain changes
- Meet Containerization — Apple’s open-source container runtime written in Swift
Comments
GitHub Issues · utterances