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 context
  • Call to main actor-isolated instance method in a synchronous nonisolated context
  • Main actor-isolated property cannot be mutated from a nonisolated context
  • Errors appear after enabling strict concurrency checking in 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 @MainActor methods from background tasks or actors
  • Nonisolated closures accessing main actor properties
  • Delegate callbacks executing on arbitrary queues
  • Protocol methods not marked with @MainActor but implementation accesses UI
  • Sendable conformance requiring isolation annotations

Step-by-Step Fix

  1. 1.Mark the entire class as @MainActor:
  2. 2.```swift
  3. 3.// Option 1: Make the whole class main actor-isolated
  4. 4.@MainActor
  5. 5.class ViewModel: ObservableObject {
  6. 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. 1.Use MainActor.run for targeted dispatch:
  2. 2.```swift
  3. 3.class ViewModel: ObservableObject {
  4. 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. 1.Fix protocol conformance with actor isolation:
  2. 2.```swift
  3. 3.// Protocol requires @MainActor
  4. 4.@MainActor
  5. 5.protocol DataSourceDelegate: AnyObject {
  6. 6.func dataSourceDidUpdate(_ source: DataSource)
  7. 7.}

// Implementation must also be @MainActor @MainActor class ViewController: UIViewController, DataSourceDelegate { func dataSourceDidUpdate(_ source: DataSource) { // Safe to update UI here tableView.reloadData() } } ```

  1. 1.Handle nonisolated closures accessing actor state:
  2. 2.```swift
  3. 3.// Before: closure captures main actor state
  4. 4.URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
  5. 5.self?.items = parse(data) // Error: main actor access from background
  6. 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.assumeIsolated for UIKit/AppKit lifecycle methods
  • Add @preconcurrency for third-party libraries not yet concurrency-safe
  • Adopt Sendable for types that cross isolation boundaries
  • Use Xcode's concurrency warnings as errors: -warn-concurrency