Swift 6.2 shipped "approachable concurrency" in late 2025, and by April 2026 it is the default experience in new Xcode 26 projects. If your SwiftUI app is still on Swift 6.0 or earlier, you are about to meet several dozen Sendable warnings the day you upgrade. This post is the practical migration guide for SwiftUI apps. What changed in 6.2, the 10 Sendable errors you will actually hit, the new @concurrent and isolatedpatterns, and the shortest path through a real-world migration.
Short version: Swift 6.2 makes strict concurrency significantly less painful than 6.0 did. Most common SwiftUI patterns now compile cleanly. The migration is mechanical rather than architectural. Plan on 4-20 hours for a typical indie SwiftUI app, depending on size and how many third-party non-Sendable dependencies you use.
What actually changed in Swift 6.2
Apple and the Swift team framed 6.2 as "approachable concurrency." The practical changes that affect SwiftUI developers:
- Main actor is the default isolation for SwiftUI-related code. Views, ObservableObject subclasses (now @Observable), and view models are main-actor-isolated by default. You no longer need to annotate every class with @MainActor.
- The
@concurrentattribute. Marks code that can run on any actor. Useful for pure data transformations, business logic, and CPU-bound work that should not block the main actor. - Better inference of Sendable. More structs, enums, and actors auto-conform to Sendable without the explicit annotation. Closures capture inference is sharper.
- The
isolatedkeyword on parameters. Lets you take an isolated value to a different actor without crossing isolation boundaries. - Reduced warnings for existing code. Many warnings that 6.0 flagged as errors are now downgraded to warnings in migration mode, giving you a gentler path.
- New
@preconcurrencyannotations in SwiftUI. Apple marked several SwiftUI APIs as pre-concurrency-safe so they do not force downstream warnings.
The 10 Sendable errors you will actually hit
After migrating a half-dozen real indie apps, these are the 10 errors I see most often and the exact fix for each.
1. "Captured variable 'foo' of non-Sendable type in a Sendable closure"
Most common. Happens when you pass a closure to Task or async function that captures a non-Sendable type.
// Fix: make the type Sendable
struct MyData: Sendable { let value: Int }
// or capture an explicit value
Task { [value = myData.value] in
await process(value)
}2. "Main actor-isolated property 'x' cannot be mutated from a non-isolated context"
Happens when background code tries to update UI state. Fix is to hop to main actor.
Task {
let result = await fetchData()
await MainActor.run {
self.items = result
}
}3. "Actor-isolated instance method cannot be referenced from a non-isolated context"
Your view model method is on the main actor, but you are calling it from a background context. Use await.
Task {
await viewModel.refresh()
}4. "Sending 'self' risks causing data races"
Closure captures self implicitly and the compiler cannot prove isolation. Explicit weak self often fixes it.
Task { [weak self] in
guard let self else { return }
await self.updateView()
}5. "Conformance of 'X' to 'Sendable' requires all stored properties to be Sendable"
You declared Sendable but one property does not conform. Find the offender, make it Sendable, or use an actor.
// Before: has a non-Sendable UIImage
struct Photo: Sendable {
let image: UIImage // error
}
// After: use Data or let the image exist outside the Sendable type
struct Photo: Sendable {
let imageData: Data
}6. "Global 'x' is not concurrency-safe"
Global variables need to be either let on a Sendable type, or isolated to an actor.
// Before
var sharedCache: [String: Data] = [:] // error in Swift 6.2
// After
actor SharedCache {
private var store: [String: Data] = [:]
func get(_ key: String) -> Data? { store[key] }
func set(_ key: String, _ value: Data) { store[key] = value }
}
let sharedCache = SharedCache()7. "Task-isolated value of type 'X' cannot be sent to main actor-isolated context"
You loaded data in a Task, now need to pass it to a main-actor-isolated handler. Convert via a Sendable value type.
// Load data off main
Task {
let raw = await apiClient.load()
let safe = raw.map { UserDTO(from: $0) } // UserDTO: Sendable
await MainActor.run {
self.users = safe
}
}8. "Passing argument of non-Sendable type from actor-isolated context"
An actor method takes a non-Sendable parameter. Fix is to make the parameter Sendable or convert before crossing.
9. "Class 'X' does not conform to the 'Sendable' protocol"
A class without annotations is not Sendable. Three ways to fix:
- Add
@MainActorto isolate it to the main actor. - Mark it as an
actorfor arbitrary isolation. - Conform to
Sendablewith@unchecked Sendableand enforce thread safety yourself.
10. "Non-Sendable type cannot cross isolation boundary"
Catch-all for cases where data moves between actors. The usual fix is to introduce a Sendable DTO type at the boundary.
The SwiftUI-specific patterns that now work cleanly
Swift 6.2 made several SwiftUI idioms compile without fighting the compiler. The important ones:
| Pattern | Swift 6.0 | Swift 6.2 |
|---|---|---|
| @Observable view model | Sometimes warnings around init | Clean, main-actor-inferred |
| View with async let | Warnings about Sendable closures | Clean when types are Sendable |
| @MainActor view model with background Task | Explicit MainActor.run needed | Still needed but clearer |
| Observable class with async method | Hard to isolate correctly | Automatic main-actor inference |
| Passing SwiftUI binding to background | Complex warnings | @preconcurrency annotation keeps it clean |
The @concurrent attribute explained
@concurrent is the new attribute for marking code that can run anywhere. Use it for:
- Pure data transformations (no UI, no shared state).
- Parsing, encoding, decoding, compression.
- Heavy computation that should not block main.
- Utility functions that do not care about isolation.
@concurrent
func parseMessages(_ data: Data) throws -> [Message] {
try JSONDecoder().decode([Message].self, from: data)
}
// Callable from any actor:
Task {
let messages = try await parseMessages(rawData)
await MainActor.run {
self.messages = messages
}
}Migrating a SwiftUI view model the right way
The canonical pattern in Swift 6.2 for a SwiftUI view model:
@MainActor
@Observable
final class HomeViewModel {
private(set) var items: [Item] = []
private(set) var isLoading = false
private let repository: ItemRepository
init(repository: ItemRepository) {
self.repository = repository
}
func load() async {
isLoading = true
defer { isLoading = false }
do {
items = try await repository.fetchItems()
} catch {
// error handling
}
}
}Notice: @MainActor on the class makes all property access and method calls main-actor-isolated, so SwiftUI views can bind without await. The async method still works because Swift understands the isolation boundaries.
Networking layers with strict concurrency
Your API client should usually be an actor or a Sendable struct with async methods. The result type must be Sendable so it can cross the boundary.
actor APIClient {
private let session: URLSession
private let decoder: JSONDecoder
init() {
self.session = URLSession.shared
self.decoder = JSONDecoder()
}
func fetch<T: Decodable & Sendable>(_ endpoint: String) async throws -> T {
let (data, _) = try await session.data(from: URL(string: endpoint)!)
return try decoder.decode(T.self, from: data)
}
}Third-party dependencies that break
Some third-party SDKs have not yet updated for Swift 6.2. Common offenders in April 2026:
- Older Firebase iOS SDK versions (pre-11.2). Upgrade to latest.
- Some KeychainAccess variants. Most are Sendable but older forks are not.
- Custom networking libraries that use closures without @Sendable.
- Old analytics SDKs that capture non-Sendable state in event closures.
Fix path: upgrade where possible, use @preconcurrency import as a temporary measure, open an issue with the library maintainer, or wrap the dependency in a Sendable facade.
Migration order that actually works
Doing strict concurrency migration in the wrong order multiplies your pain. The order that works for a real indie SwiftUI app:
- Step 1: enable strict concurrency in Xcode settings. Set SWIFT_STRICT_CONCURRENCY to complete. Build, note warnings but do not fix yet.
- Step 2: annotate your view models first. Add
@MainActorto every view model that touches UI. This eliminates roughly 40 percent of warnings in one pass. - Step 3: fix your networking layer. Convert your API client to an actor. Mark response types Sendable.
- Step 4: fix your persistence layer. Actor-ize database wrappers, ensure stored types are Sendable.
- Step 5: address remaining warnings in app code. Usually individual fixes now that the infrastructure is clean.
- Step 6: address third-party SDK warnings. Upgrade or wrap. Use @preconcurrency imports as last resort.
- Step 7: run full test suite and manual smoke test. Strict concurrency can surface subtle race conditions that only show up at runtime.
Testing strict concurrency code
Swift Testing (the successor to XCTest) handles concurrency better than XCTest. Three rules:
- Use
@MainActoron tests that interact with UI or view models. - Use
awaitliberally. Async tests are first-class. - Use
Task.sleep(for:)orContinuousClockinstead ofThread.sleep.
What The Swift Kit ships
The Swift Kit is built clean against Swift 6.2 strict concurrency. Every view model is @MainActor @Observable. The API client, persistence wrapper, and analytics layer are actors. All public types are explicitly Sendable. You get zero strict concurrency warnings out of the box and a clean foundation for your own features.
$99 one-time, unlimited commercial projects. See every integration on the features page or jump to pricing.
Final recommendation
If you ship SwiftUI apps in 2026, do the Swift 6.2 migration. The pain is real but bounded (4-20 hours for most indie apps), the payoff is lifetime: clean code, fewer race conditions, and compatibility with Xcode 26.3 agentic coding which assumes strict concurrency. Follow the 7-step order above, use the Swift Kit as a reference for patterns, and you will be through the migration in a focused weekend.