Knowledge
Search knowledge... ⌘K
Knowledge · Guidelines · swift
Swift Actor Sendable Boundary
Cross the actor → @MainActor boundary safely in Swift 6 strict concurrency. Keep view models as plain Sendable data, never pass non-Sendable types (NSAttributedString, NSView, NSButton, UIView) across isolation domains, and avoid holding @MainActor references from within actor-isolated code.
Metadata
swift swift mandatory
Procedures
Showing 3 of 5
- 1 Make every ViewModel a Sendable struct of plain data
```swift struct RecordingViewModel: Equatable, Sendable { let statusLabel: String let stableText: String // plain String — not NSAttributedString let tentativeText: String // plain String let recordButtonEnabled: Bool let isRecording: Bool // ... all fields are Sendable value types or protocol-bound Sendable types } ``` The view layer builds the NSAttributedString LOCALLY from `(stableText, tentativeText, themeTokens)` in the view's render method. Non-Sendable types never cross the boundary. - 2 Hop from actor to @MainActor via `Task { @MainActor in ... }` or `await MainActor.run { ... }`
```swift actor TranscriptionService { func handleStreamUpdate(stable: String, tentative: String) { // ... actor work ... let snapshotStable = stable // captured by value let snapshotTentative = tentative Task { @MainActor in AppContainer.current.resolve(RecordingStateMachine.self) .handle(.streamUpdate(stable: snapshotStable, tentative: snapshotTentative)) } } } ``` - 3 Never store a @MainActor-isolated class inside an actor
BAD: ```swift actor TranscriptionService { let machine: RecordingStateMachine // ERROR: @MainActor type in actor } ``` GOOD — the actor holds a Sendable closure provided by the composition root. The closure internally hops to MainActor: ```swift actor TranscriptionService { let dispatch: @Sendable (RecordingEvent) -> Void init(dispatch: @escaping @Sendable (RecordingEvent) -> Void) { self.dispatch = dispatch } func handleStreamUpdate(...) { dispatch(.streamUpdate(...)) } } // In AppCoordinator (composition root): let machine = RecordingStateMachine(...) let service = TranscriptionService { event in Task { @MainActor in machine.handle(event) } } ```