rshankras/swift-concurrency-updates
Swift 6.2 concurrency updates including default MainActor inference, @concurrent for background work, isolated conformances, and approachable concurrency migration. Use when adopting Swift 6.2 concurrency features or fixing data-race errors.
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill swift-concurrency-updates
Swift 6.2 introduces "Approachable Concurrency" -- a set of changes that make strict concurrency dramatically easier to adopt. Code runs on @MainActor by default, async functions stay on the calling actor, and you explicitly request background execution with @concurrent.
This skill covers only the Swift 6.2 specific changes. For general concurrency patterns (actors, TaskGroup, AsyncSequence, Sendable, cancellation), see the swift/concurrency-patterns skill.
@concurrent, isolated conformances, or approachable concurrency| Feature | Before (6.0/6.1) | After (6.2) |
|---------|------------------|-------------|
| Default isolation | Nothing inferred; manual @MainActor everywhere | Opt-in mode infers @MainActor on everything |
| Async function execution | Hops to generic concurrent executor | Stays on calling actor |
| @MainActor type conforming to protocol | Compiler error for non-isolated protocols | Isolated conformances: @MainActor Protocol |
| Background execution | Task.detached or manual nonisolated functions | @concurrent attribute |
| Global/static mutable state | Required @MainActor annotation or Sendable | Default MainActor mode handles it automatically |
In Swift 6.2, async functions without specific actor isolation stay on whatever actor called them, instead of hopping to the generic concurrent executor as in 6.0/6.1 -- eliminating a common source of data-race errors with no code changes required.
An opt-in build setting that makes all code implicitly @MainActor unless explicitly opted out with nonisolated.
Xcode: Build Settings > Swift Compiler - Concurrency > "Default Actor Isolation" > "MainActor"
Swift Package Manager:
.executableTarget(
name: "MyApp",
swiftSettings: [
.defaultIsolation(MainActor.self)
]
)
With this enabled, app-level types no longer need explicit @MainActor annotations -- including global/static mutable state, which otherwise needs an explicit @MainActor static let ....
| Target Type | Recommended? | Reason |
|-------------|-------------|--------|
| App target | Yes | Apps are UI-driven; most code belongs on MainActor |
| Script / executable | Yes | Scripts are sequential; MainActor default is natural |
| Library / framework | No | Libraries must not impose actor isolation on consumers |
| Package plugin | No | Same reasoning as libraries |
When a type or function genuinely needs to run off the main actor, mark it nonisolated:
nonisolated struct ImageProcessor {
func processImage(_ data: Data) -> UIImage {
// Runs on any thread, not MainActor
...
}
}
Isolated conformances let a @MainActor type conform to a protocol that does not require actor isolation, using extension Type: @MainActor ProtocolName. Before Swift 6.2, this produced a compiler error about the main actor-isolated conformance crossing an isolation boundary.
protocol Exportable {
func export()
}
@MainActor
final class StickerModel {
let processor: PhotoProcessor
func doExport() { processor.exportAsPNG() }
}
// ✅ Swift 6.2 -- isolated conformance
extension StickerModel: @MainActor Exportable {
func export() {
processor.exportAsPNG() // Works: conformance is MainActor-isolated
}
}
The conformance can only be used from a context that shares the same isolation domain:
// ✅ Used within @MainActor context -- OK
@MainActor
func exportAll(_ items: [any Exportable]) {
for item in items { item.export() }
}
// ❌ Used outside @MainActor -- compile error
nonisolated func exportAll(_ items: [any Exportable]) {
for item in items {
item.export() // Error: isolated conformance not available here
}
}
@concurrent explicitly offloads a function to the background thread pool, replacing the pattern of using Task.detached for compute-intensive operations.
nonisolated struct ImageProcessor {
@concurrent
func resize(image: Data, to size: CGSize) async -> Data {
// Runs on background thread pool
...
}
}
// Caller (on MainActor):
let resized = await ImageProcessor().resize(image: data, to: targetSize)
nonisolated (for structs/classes; actors are already isolated)@concurrent to the functionasyncawait| Mechanism | Use Case | Structured? |
|-----------|----------|-------------|
| @concurrent | Single function that must run on background thread | Yes (inherits task context) |
| Task.detached | Fire-and-forget background work, no structured parent | No |
| actor | Shared mutable state needing serialized access | N/A (isolation, not scheduling) |
| Task {} | Unstructured task inheriting current actor | No |
Reserve it for CPU-intensive work, blocking I/O that would freeze the UI, or work measured to be long enough to justify the thread hop -- not trivial functions:
// ❌ Wrong -- trivial work does not need @concurrent
nonisolated struct UserFormatter {
@concurrent
func formatName(_ user: User) async -> String {
return "\(user.firstName) \(user.lastName)"
}
}
// ✅ Right -- leave it on the calling actor
struct UserFormatter {
func formatName(_ user: User) -> String {
return "\(user.firstName) \(user.lastName)"
}
}
Step 1: Update to Swift 6.2 toolchain
Ensure your Xcode version supports Swift 6.2 and your project's Swift language version is set to 6.2.
Step 2: Enable default MainActor inference (for app targets)
See "Enabling It" above -- the Xcode build setting or .defaultIsolation(MainActor.self) in swiftSettings.
Step 3: Remove redundant @MainActor annotations
With default MainActor inference enabled, explicit @MainActor annotations on app-level types are redundant and can be removed to reduce noise.
Step 4: Replace Task.detached with @concurrent where appropriate
Mark the offloaded function @concurrent directly instead of wrapping it in a detached task inside a TaskGroup.
Step 5: Fix remaining conformance errors with isolated conformances
Part of migrating to Swift 6.2 involves replacing unsafe @unchecked Sendable workarounds with isolated conformances:
// Before -- unsafe workaround
extension MyModel: @unchecked Sendable {}
// After -- isolated conformance
extension MyModel: @MainActor Exportable {
func export() { ... }
}
Step 6: Add nonisolated to types/functions that must not be on MainActor
With default MainActor inference, anything not explicitly marked nonisolated runs on MainActor. Audit background data processing types, network parsers, file I/O utilities, and computation-heavy algorithms.
nonisolated struct JSONParser {
@concurrent
func parse(_ data: Data) async throws -> [Model] { ... }
}
~/Downloads/docs/Swift-Concurrency-Updates.md — read if present; skip silently if absent.Swift 6.2 Concurrency Defaults
+--------------------------------------------+
| Everything is @MainActor by default |
| (with "infer main actor" build setting) |
| |
| Async functions stay on the calling actor |
| (no implicit hop to background) |
| |
| Use @concurrent to explicitly go background|
| Use nonisolated to opt out of MainActor |
+--------------------------------------------+
Progression:
1. Write code -> runs on MainActor -> no data races
2. Use async/await -> stays on calling actor -> still no races
3. Need parallelism -> @concurrent -> explicit, auditable
4. Need shared state -> actor -> serialized access
Libraries should let consumers choose their own isolation strategy. Only app targets and executables should use default MainActor inference.
// ❌ Wrong -- library imposes MainActor on all consumers
// Package.swift
.target(
name: "MyNetworkingLib",
swiftSettings: [
.defaultIsolation(MainActor.self) // Do NOT do this for libraries
]
)
Every @concurrent call involves a thread hop. Only use it for genuinely expensive work (see "When NOT to Use @concurrent" above).
// With default MainActor inference enabled:
// ❌ Wrong -- this CPU-intensive parser now runs on MainActor, blocking UI
struct LargeFileParser {
func parse(_ data: Data) -> [Record] {
...
}
}
// ✅ Right -- opt out of MainActor for background-suitable types
nonisolated struct LargeFileParser {
@concurrent
func parse(_ data: Data) async -> [Record] {
...
}
}
See "Isolated Conformances" above -- the conformance is only usable from a context that matches its isolation.
// ❌ Wrong -- removed annotations but did NOT enable default MainActor inference
class ViewModel { // No longer @MainActor -- state is unprotected
var items: [Item] = []
func load() async { ... }
}
// ✅ Right -- either keep the annotation...
@MainActor
class ViewModel {
var items: [Item] = []
func load() async { ... }
}
// ...or enable "Default Actor Isolation: MainActor" in build settings
// and then annotations are unnecessary
class ViewModel {
var items: [Item] = []
func load() async { ... }
}
When reviewing code that uses or should use Swift 6.2 concurrency features:
.defaultIsolation(MainActor.self)@MainActor annotations removed (if default MainActor inference is enabled)nonisolated applied to types/functions that must run off the main actornonisolated and uses @concurrent@concurrent only used for genuinely expensive operations@concurrent functions are asyncnonisolated (for structs/classes)Task.detached replaced with @concurrent where structured concurrency is preferable@MainActor Protocol syntax used for MainActor types conforming to non-isolated protocols@unchecked Sendable workarounds that isolated conformances can replace@unchecked Sendable conformances from pre-6.2 workaroundsTask.detached calls (prefer @concurrent or structured concurrency)swift/concurrency-patternsswift/concurrency-patterns/actors-and-isolation.mdswift/concurrency-patterns/structured-concurrency.mdswift/concurrency-patterns/migration-guide.md~/Downloads/docs/Swift-Concurrency-Updates.md — read if present; skip silently if absent.Take rshankras/swift-concurrency-updates from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.