Skip to content

Adopting Swift 6 (SwiftUI)

A SwiftUI guide for coding agents. Also covers swift 6 migration, sendable errors, data race safety, mainactor view model, actor for shared state, exc_bad_access concurrency.

Swift 6 language mode turns data-race risks into compiler errors, mostly about sendability. The practical strategy: sort your types into four buckets and give each bucket one concurrency tool.

Data types: immutable Sendable structs

What cannot mutate cannot race. Model data as structs with let properties. Internal structs infer Sendable; a public struct must declare it explicitly.

public struct Statistics: Sendable, Hashable {
  public let value: Double
  public let interval: DateInterval
}

Stateless services: structs too

A service that holds no mutable state, only a handle it reads through, can also be a Sendable struct and cross threads freely.

public struct HealthService: Sendable {
  private let store: HKHealthStore

  public var age: Int {
    // derive from store.dateOfBirthComponents()
  }
}

View-facing classes: isolate to @MainActor

Use classes only where state must be shared without copying, typically view models and delegates. Annotating them @MainActor makes them implicitly sendable because a global actor serializes all access.

@MainActor @Observable final class Store<State, Action> {
  private(set) var state: State
  private let reduce: (State, Action) -> State

  func send(_ action: Action) {
    state = reduce(state, action)
  }
}

Stateful services: actors

A service that both holds state and is called from many threads, such as a search service with a cache, is the real data-race source. The actor keyword gives it mutually exclusive access.

actor SearchService {
  private var history = Tree<Food>()

  func search() async throws -> [Food] {
    // read history, fetch, mutate history
  }
}

The symptom this whole scheme prevents: a class shared across threads, one of them writing, and the app dying with EXC_BAD_ACCESS.

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-swiftui-guide({ topic: "swiftui-adopting-swift6-across-the-app-codebase" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems