NewThe Flutter Kit — Flutter boilerplate$149$69
Guide

Swift 6.2 Approachable Concurrency: SwiftUI Migration Guide (2026)

The 2026 SwiftUI migration guide. What changed in 6.2, how to fix the 10 most common Sendable errors, new @concurrent and isolated patterns, and how The Swift Kit handles strict concurrency out of the box.

Ahmed GaganAhmed Gagan
17 min read

Skip 100+ hours of setup. Get The Swift Kit $149 $99 one-time

Get it now →

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 @concurrent attribute. 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 isolated keyword 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 @preconcurrency annotations 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 @MainActor to isolate it to the main actor.
  • Mark it as an actor for arbitrary isolation.
  • Conform to Sendable with @unchecked Sendable and 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:

PatternSwift 6.0Swift 6.2
@Observable view modelSometimes warnings around initClean, main-actor-inferred
View with async letWarnings about Sendable closuresClean when types are Sendable
@MainActor view model with background TaskExplicit MainActor.run neededStill needed but clearer
Observable class with async methodHard to isolate correctlyAutomatic main-actor inference
Passing SwiftUI binding to backgroundComplex 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 @MainActor to 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 @MainActor on tests that interact with UI or view models.
  • Use await liberally. Async tests are first-class.
  • Use Task.sleep(for:) or ContinuousClock instead 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.

Share this article
Limited-time · price rises to $149 soon

Ready to ship your iOS app faster?

The Swift Kit gives you a production-ready SwiftUI codebase with onboarding, paywalls, auth, AI integrations, and a full design system. Stop rebuilding boilerplate — start building your product.

$149$99one-time · save $50
  • Full source code
  • Unlimited projects
  • Lifetime updates
  • 50+ makers shipping