Introduction
Swift's concurrency model uses actors to protect mutable state from data races. The @MainActor attribute marks code that must run on the main thread, particularly UI-related code. With Swift 5.10's complete concurrency checking and Swift 6's strict mode, the compiler enforces actor isolation at compile time, rejecting any access to @MainActor-isolated properties from non-isolated contexts.
Symptoms
Property declared in main actor-isolated context cannot be referenced from nonisolated contextCall to main actor-isolated instance method in a synchronous nonisolated contextMain actor-isolated property cannot be mutated from a nonisolated context- Errors appear after enabling
strict concurrency checkingin build settings - Code compiled fine in Swift 5.7 but errors in Swift 5.10+
Example error: ```swift class ViewModel: ObservableObject { @MainActor @Published var items: [Item] = []
// Error: Main actor-isolated property 'items' cannot be mutated // from a nonisolated context func loadItems() { Task { let fetched = try await api.fetchItems() items = fetched // ERROR: not on main actor } } } ```
Common Causes
- Calling
@MainActormethods from background tasks or actors - Nonisolated closures accessing main actor properties
- Delegate callbacks executing on arbitrary queues
- Protocol methods not marked with
@MainActorbut implementation accesses UI Sendableconformance requiring isolation annotations
Step-by-Step Fix
- 1.Mark the entire class as @MainActor:
- 2.```swift
- 3.// Option 1: Make the whole class main actor-isolated
- 4.@MainActor
- 5.class ViewModel: ObservableObject {
- 6.@Published var items: [Item] = []
func loadItems() async { do { let fetched = try await api.fetchItems() // Can access items directly - already on main actor items = fetched } catch { print("Failed: \(error)") } } } ```
- 1.Use MainActor.run for targeted dispatch:
- 2.```swift
- 3.class ViewModel: ObservableObject {
- 4.@Published var items: [Item] = []
func loadItems() async { do { let fetched = try await api.fetchItems()
// Switch to main actor for mutation await MainActor.run { items = fetched } } catch { await MainActor.run { showError(error) } } } } ```
- 1.Fix protocol conformance with actor isolation:
- 2.```swift
- 3.// Protocol requires @MainActor
- 4.@MainActor
- 5.protocol DataSourceDelegate: AnyObject {
- 6.func dataSourceDidUpdate(_ source: DataSource)
- 7.}
// Implementation must also be @MainActor @MainActor class ViewController: UIViewController, DataSourceDelegate { func dataSourceDidUpdate(_ source: DataSource) { // Safe to update UI here tableView.reloadData() } } ```
- 1.Handle nonisolated closures accessing actor state:
- 2.```swift
- 3.// Before: closure captures main actor state
- 4.URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
- 5.self?.items = parse(data) // Error: main actor access from background
- 6.}.resume()
// After: dispatch to main actor URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in let parsed = parse(data) // Background work Task { @MainActor [weak self] in self?.items = parsed // Main thread UI update } }.resume() ```
Prevention
- Enable "Complete Concurrency Checking" in Xcode build settings
- Mark UI-related classes with
@MainActor - Use
MainActor.assumeIsolatedfor UIKit/AppKit lifecycle methods - Add
@preconcurrencyfor third-party libraries not yet concurrency-safe - Adopt
Sendablefor types that cross isolation boundaries - Use Xcode's concurrency warnings as errors:
-warn-concurrency