skills/swift/concurrency/SKILL.md
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 skillsauth add rshankras/claude-code-apple-skills swift-concurrency-updatesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Swift 6.2 introduces "Approachable Concurrency" -- a set of changes that make strict concurrency dramatically easier to adopt. The philosophy shifts from "opt in to safety" to "safe by default, opt in to concurrency." 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 concurrencyIn Swift 6.0 and 6.1, strict concurrency was correct but painful. Developers faced walls of data-race compiler errors that were difficult to resolve. Non-actor-annotated async functions would eagerly hop to the generic concurrent executor, causing unexpected data races when passing mutable state. Conforming @MainActor types to non-isolated protocols was often impossible without workarounds.
| 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.0/6.1, a non-actor-annotated async function called from a @MainActor context would hop off the main actor to the generic concurrent executor. This caused data-race errors when the caller passed non-Sendable state.
In Swift 6.2, async functions without specific actor isolation stay on whatever actor they are called from. No hop, no data race.
// ❌ Swift 6.0/6.1 -- ERROR: Sending 'self.processor' risks causing data races
@MainActor
final class StickerModel {
let processor = PhotoProcessor()
func extract(_ item: PhotosPickerItem) async throws -> Sticker? {
let data = try await item.loadTransferable(type: Data.self)
// processor hops off MainActor -- data race
return await processor.extractSticker(data: data, with: item.itemIdentifier)
}
}
class PhotoProcessor {
func extractSticker(data: Data, with id: String?) async -> Sticker? {
// This runs on the concurrent executor in 6.0/6.1
...
}
}
// ✅ Swift 6.2 -- No error. extractSticker stays on the caller's actor.
@MainActor
final class StickerModel {
let processor = PhotoProcessor()
func extract(_ item: PhotosPickerItem) async throws -> Sticker? {
let data = try await item.loadTransferable(type: Data.self)
// processor stays on MainActor -- no data race
return await processor.extractSticker(data: data, with: item.itemIdentifier)
}
}
class PhotoProcessor {
func extractSticker(data: Data, with id: String?) async -> Sticker? {
// In 6.2, this runs on MainActor because the caller is @MainActor
...
}
}
Why this matters: Many data-race errors in Swift 6.0/6.1 were caused by this implicit hop. In 6.2, the same code compiles cleanly with no changes needed.
An opt-in build setting that makes all code implicitly @MainActor unless explicitly opted out with nonisolated. This eliminates the vast majority of data-race errors for single-threaded app code.
Xcode: Build Settings > Swift Compiler - Concurrency > "Default Actor Isolation" > "MainActor"
Swift Package Manager:
.executableTarget(
name: "MyApp",
swiftSettings: [
.defaultIsolation(MainActor.self)
]
)
With this mode enabled, you no longer need @MainActor annotations on app-level types:
// ❌ Before (Swift 6.0/6.1) -- manual @MainActor annotations everywhere
@MainActor
final class StickerLibrary {
static let shared: StickerLibrary = .init()
}
@MainActor
final class StickerModel {
let processor: PhotoProcessor
var selection: [PhotosPickerItem]
}
@MainActor
struct ContentView: View {
@State private var model = StickerModel()
var body: some View { ... }
}
// ✅ After (Swift 6.2 with default MainActor inference) -- no annotations needed
final class StickerLibrary {
static let shared: StickerLibrary = .init() // Implicitly @MainActor
}
final class StickerModel {
let processor: PhotoProcessor // Implicitly @MainActor
var selection: [PhotosPickerItem]
}
struct ContentView: View {
@State private var model = StickerModel()
var body: some View { ... }
}
| 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:
// With "infer main actor" enabled, use nonisolated to opt out:
nonisolated struct ImageProcessor {
func processImage(_ data: Data) -> UIImage {
// Runs on any thread, not MainActor
...
}
}
nonisolated func heavyComputation() -> Result {
// Runs on any thread
...
}
With default MainActor inference enabled, global and static mutable state is automatically protected:
// ❌ Before -- required explicit annotation or Sendable conformance
@MainActor static let shared: StickerLibrary = .init()
// ✅ After -- default MainActor inference handles it
static let shared: StickerLibrary = .init() // Implicitly @MainActor
Without default MainActor inference, you can still protect individual declarations:
@MainActor static let shared: StickerLibrary = .init()
Allows @MainActor types to conform to protocols that do not require actor isolation. Before Swift 6.2, this was a common source of frustrating compiler errors.
protocol Exportable {
func export()
}
@MainActor
final class StickerModel {
let processor: PhotoProcessor
func doExport() {
processor.exportAsPNG()
}
}
// ❌ Swift 6.0/6.1 -- ERROR: Main actor-isolated conformance crosses isolation boundary
extension StickerModel: Exportable {
func export() {
processor.exportAsPNG() // Needs MainActor, but protocol is non-isolated
}
}
// ✅ Swift 6.2 -- Isolated conformance
extension StickerModel: @MainActor Exportable {
func export() {
processor.exportAsPNG() // Works: conformance is MainActor-isolated
}
}
The compiler enforces that isolated conformances are only used in matching isolation contexts:
// ✅ Used within @MainActor context -- OK
@MainActor
struct ImageExporter {
var items: [any Exportable]
mutating func add(_ item: StickerModel) {
items.append(item) // OK: both are @MainActor
}
}
// ❌ Used outside @MainActor -- compile error
nonisolated struct ImageExporter {
var items: [any Exportable]
mutating func add(_ item: StickerModel) {
items.append(item) // Error: Main actor-isolated conformance
// cannot be used in nonisolated context
}
}
The conformance is not universally available. It only works when the caller shares the same isolation domain.
When you need true parallelism for CPU-heavy work, use @concurrent to explicitly offload to the background thread pool. This replaces the pattern of using Task.detached for compute-intensive operations.
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) // Background execution
cachedStickers[id] = sticker
return sticker
}
@concurrent
static func extractSubject(from data: Data) async -> Sticker {
// Heavy image processing -- runs on concurrent thread pool
...
}
}
nonisolated (for structs/classes; actors are already isolated)@concurrent to the functionasyncawaitnonisolated 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)
| 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 |
Prefer @concurrent for compute-heavy functions. Prefer actors for shared state. Avoid Task.detached when @concurrent or structured concurrency works.
Do not mark every async function as @concurrent. Most app code should stay on the calling actor. Only use @concurrent when:
// ❌ Wrong -- trivial work does not need @concurrent
nonisolated struct UserFormatter {
@concurrent
func formatName(_ user: User) async -> String { // Unnecessary thread hop
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)
Xcode: Build Settings > Swift Compiler - Concurrency > Default Actor Isolation > MainActor
Swift Package Manager:
.executableTarget(
name: "MyApp",
swiftSettings: [
.defaultIsolation(MainActor.self)
]
)
Step 3: Remove redundant @MainActor annotations
With default MainActor inference enabled, explicit @MainActor annotations on app-level types are redundant. Remove them to reduce noise:
// Before
@MainActor class ViewModel { ... }
@MainActor struct ContentView: View { ... }
// After (with default MainActor inference)
class ViewModel { ... }
struct ContentView: View { ... }
Step 4: Replace Task.detached with @concurrent where appropriate
// Before
func processImages(_ data: [Data]) async -> [UIImage] {
await withTaskGroup(of: UIImage.self) { group in
for item in data {
group.addTask { // Task.detached implied hop
await self.decode(item)
}
}
return await group.reduce(into: []) { $0.append($1) }
}
}
// After
@concurrent
func decode(_ data: Data) async -> UIImage {
// Explicitly runs on background
...
}
Step 5: Fix remaining conformance errors with isolated conformances
// Before -- workaround with @unchecked Sendable or nonisolated
extension MyModel: @unchecked Sendable {} // Unsafe workaround
// 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 your code for:
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
// ❌ Wrong -- library imposes MainActor on all consumers
// Package.swift
.target(
name: "MyNetworkingLib",
swiftSettings: [
.defaultIsolation(MainActor.self) // Do NOT do this for libraries
]
)
Libraries should let consumers choose their own isolation strategy. Only app targets and executables should use default MainActor inference.
// ❌ Wrong -- unnecessary thread hop for trivial work
@concurrent
func greet(_ name: String) async -> String {
"Hello, \(name)"
}
// ✅ Right -- no @concurrent needed
func greet(_ name: String) -> String {
"Hello, \(name)"
}
Every @concurrent call involves a thread hop. Only use it for genuinely expensive work.
// With default MainActor inference enabled:
// ❌ Wrong -- this CPU-intensive parser now runs on MainActor, blocking UI
struct LargeFileParser {
func parse(_ data: Data) -> [Record] {
// Heavy parsing blocks the main thread
...
}
}
// ✅ Right -- opt out of MainActor for background-suitable types
nonisolated struct LargeFileParser {
@concurrent
func parse(_ data: Data) async -> [Record] {
// Runs on background thread pool
...
}
}
extension MyModel: @MainActor Exportable {
func export() { ... }
}
// ❌ Wrong -- trying to use the conformance from a nonisolated context
nonisolated func exportAll(_ items: [any Exportable]) {
for item in items {
item.export() // Compiler error: isolated conformance not available here
}
}
// ✅ Right -- use the conformance from a matching isolation context
@MainActor
func exportAll(_ items: [any Exportable]) {
for item in items {
item.export() // OK: both are @MainActor
}
}
// ❌ 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 annotations OR enable the build setting
// Option A: Keep the annotation
@MainActor
class ViewModel {
var items: [Item] = []
func load() async { ... }
}
// Option B: Enable "Default Actor Isolation: MainActor" in build settings
// 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.development
US web checkout via the StoreKit External Purchase Link entitlement — currently 0% Apple commission (litigation ongoing), how to ship it safely, and how to architect for a commission flip so a future ruling is a config change, not a rewrite. Use when adding external purchase links, weighing web checkout vs IAP, or planning US-storefront pricing strategy.
tools
Revenue beyond the single-app price tag — own-app bundles, Family Sharing as a conversion lever, cross-developer bundles & suites, and institutional licensing via Group Purchases / Apple School & Business Manager. Use when a developer has multiple apps, a subscription worth sharing, complementary indie partners, or school/clinic/business buyers.
testing
Run a structured accessibility audit on an iOS/macOS app — automated XCUITest audits, Accessibility Inspector, manual VoiceOver/Dynamic Type passes, and App Store Accessibility Nutrition Label evaluation. Use before release, when preparing Nutrition Label declarations, or for EU Accessibility Act compliance.
tools
Stage-by-stage audit of an app's App Store growth machinery against a 54-item P0–P9 playbook — every item scored from an App Store Connect MCP call, a codebase check, or an explicit question to the user, then routed to the skill or command that fixes it. Read-only on App Store Connect. Use for a growth audit or scorecard, a pre-launch growth plan, a quarterly re-audit, or "which growth levers am I missing."