Patterns adopting the Swift 6.2 concurrency model: code runs single-threaded by default, and concurrency must be introduced explicitly. Eliminates common data-race errors without sacrificing performance.
When to Activate
- Migrating a Swift 5.x or 6.0/6.1 project to Swift 6.2
- Resolving compiler errors about data race safety
- Designing an app architecture based on
MainActorapplication architecture - Offloading CPU-intensive work to background threads
- Implementing protocol conformances on types isolated to
MainActorImplement protocol conformances on isolated types - Enabling the Approachable Concurrency build setting in Xcode 26
Core Problem: Implicit Background Offloading
In Swift 6.1 and earlier, async functions could be implicitly offloaded to a background thread, causing data race errors even in seemingly safe code:
// Swift 6.1: 错误 (ERROR)
@MainActor
final class StickerModel {
let photoProcessor = PhotoProcessor()
func extractSticker(_ item: PhotosPickerItem) async throws -> Sticker? {
guard let data = try await item.loadTransferable(type: Data.self) else { return nil }
// 错误:发送 'self.photoProcessor' 存在引起数据竞争的风险
return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
}
}
Swift 6.2 fixes this: async functions stay on the caller's execution actor by default.
// Swift 6.2: 正常 (OK) — 异步保留在 MainActor,无数据竞争
@MainActor
final class StickerModel {
let photoProcessor = PhotoProcessor()
func extractSticker(_ item: PhotosPickerItem) async throws -> Sticker? {
guard let data = try await item.loadTransferable(type: Data.self) else { return nil }
return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
}
}
Core Pattern — Isolated Conformances
MainActor types can now safely conform to nonisolated protocols:
protocol Exportable {
func export()
}
// Swift 6.1: 错误 (ERROR) — 跨入主执行角色隔离的代码
// Swift 6.2: 正常 (OK),使用隔离一致性
extension StickerModel: @MainActor Exportable {
func export() {
photoProcessor.exportAsPNG()
}
}
The compiler ensures this conformance is only used on the Main Actor:
// 正常 (OK) — ImageExporter 也是 @MainActor
@MainActor
struct ImageExporter {
var items: [any Exportable]
mutating func add(_ item: StickerModel) {
items.append(item) // 安全:相同的执行角色隔离
}
}
// 错误 (ERROR) — 非隔离上下文不能使用 MainActor 一致性
nonisolated struct ImageExporter {
var items: [any Exportable]
mutating func add(_ item: StickerModel) {
items.append(item) // 错误:此处无法使用 Main actor 隔离的一致性
}
}
Core Pattern — Global and Static Variables
Use MainActor to protect global/static state:
// Swift 6.1: 错误 (ERROR) — 非 Sendable 类型可能具有共享的可变状态
final class StickerLibrary {
static let shared: StickerLibrary = .init() // 错误
}
// 修复:添加 @MainActor 注解
@MainActor
final class StickerLibrary {
static let shared: StickerLibrary = .init() // 正常 (OK)
}
MainActor Default Inference Mode
Swift 6.2 introduces a mode that infers MainActor by default — no manual annotations needed:
// 启用 MainActor 默认推断后:
final class StickerLibrary {
static let shared: StickerLibrary = .init() // 隐式为 @MainActor
}
final class StickerModel {
let photoProcessor: PhotoProcessor
var selection: [PhotosPickerItem] // 隐式为 @MainActor
}
extension StickerModel: Exportable { // 隐式为 @MainActor 一致性
func export() {
photoProcessor.exportAsPNG()
}
}
This mode is optional and recommended for apps, scripts, and other executable targets.
Core Pattern — Background Work with @concurrent
When you need true parallelism, use @concurrent to explicitly offload:
Important Note: This example requires the Approachable Concurrency build settings — SE-0466 (MainActor default isolation) and SE-0461 (nonisolated(nonsending) by default). With these settings enabled,
extractStickerstays on the caller's execution actor, making mutable state access safe.Without these settings, this code would have a data race — the compiler will report an error for it.
nonisolated final class PhotoProcessor {
private 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 { /* ... */ }
}
// 调用方必须使用 await
let processor = PhotoProcessor()
processedPhotos[item.id] = await processor.extractSticker(data: data, with: item.id)
Steps for using @concurrent :
- Mark the containing type as
nonisolated - Add
@concurrent - to the function
async - If not already async, add
await
Key Design Decisions
| Decision | Rationale |
|---|---|
| Single-threaded by default | Most naturally written code is free of data races; concurrency is optional |
| Async stays on the calling execution actor | Eliminates implicit offloading that causes data race errors |
| Isolated conformances | MainActor types can conform to protocols without unsafe workarounds |
@concurrent explicit opt-in |
Background execution is a deliberate performance choice, not an accident |
MainActor default inference |
Reduces redundant @MainActor annotations in app targets |
| Opt-in adoption | Non-destructive migration path — enable features incrementally |
Migration steps
- Enable in Xcode: Swift Compiler > Concurrency section in Build Settings
- Enable in SPM: use in the package manifest
SwiftSettingsAPI - Use migration tools: automated code changes via swift.org/migration
- Start with MainActor by default: enable inferred mode for App targets
- Add where needed
@concurrent: Profile first, then offload hot paths - Test thoroughly: data race issues will become compile-time errors
Best practices
- Start with MainActor — write single-threaded code first, optimize later
- Use only for CPU-intensive work
@concurrent— image processing, compression, complex computations - Enable MainActor inference mode for App targets that are mostly single-threaded
- Profile before offloading — use Instruments to find real bottlenecks
- Use MainActor to protect global variables — global/static mutable state requires actor isolation
- Use isolated conformances, not
nonisolatedworkarounds or@Sendablewrappers - Incremental migration — enable one feature at a time in Build Settings
Anti-Patterns to avoid
- Apply to every async function
@concurrent(most functions don't need background execution) - Use without understanding isolation
nonisolatedto suppress compiler errors - Keep the old when actors provide the same safety
DispatchQueuepatterns - Skip in concurrency-related Foundation Models code
model.availabilitychecks - Fight the compiler — if it reports a data race, the code has a real concurrency problem
- Assume all async code runs in the background (Swift 6.2 default: stays on the calling actor)
When to use
- All new Swift 6.2+ projects (recommended default: "easy concurrency")
- Migrate existing Apps from Swift 5.x or 6.0/6.1 concurrency
- Resolve data race safety compiler errors during Xcode 26 adoption
- Build application architecture centered on
MainActor(most UI Apps) - Performance optimization — offload specific heavy computations to the background