--- name: axiom-assume-isolated description: Use when needing synchronous actor access in tests, legacy delegate callbacks, or performance-critical code. Covers MainActor.assumeIsolated, @preconcurrency protocol conformances, crash behavior, Task vs assumeIsolated. license: MIT metadata: version: "1.0.0" --- # assumeIsolated — Synchronous Actor Access Synchronously access actor-isolated state when you **know** you're already on the correct isolation domain. ## When to Use ✅ **Use when:** - Testing MainActor code synchronously (avoiding Task overhead) - Legacy delegate callbacks documented to run on main thread - Performance-critical code avoiding async hop overhead - Protocol conformances where callbacks are guaranteed on specific actor ❌ **Don't use when:** - Uncertain about current isolation (use `await` instead) - Already in async context (you have isolation) - Cross-actor calls needed (use async) - Callback origin is unknown or untrusted ## API Reference ### MainActor.assumeIsolated ```swift static func assumeIsolated( _ operation: @MainActor () throws -> T, file: StaticString = #fileID, line: UInt = #line ) rethrows -> T where T: Sendable ``` **Behavior**: Executes synchronously. **Crashes** if not on MainActor's serial executor. ### Custom Actor assumeIsolated ```swift func assumeIsolated( _ operation: (isolated Self) throws -> T, file: StaticString = #fileID, line: UInt = #line ) rethrows -> T where T: Sendable ``` ## Task vs assumeIsolated | Aspect | `Task { @MainActor in }` | `MainActor.assumeIsolated` | |--------|--------------------------|---------------------------| | Timing | Deferred (next run loop) | Synchronous (inline) | | Async support | Yes (can await) | No (sync only) | | Context | From any context | Must be sync function | | Failure mode | Runs anyway | **Crashes** if wrong isolation | | Use case | Start async work | Verify + access isolated state | ## Patterns ### Pattern 1: Testing MainActor Code ```swift @Test func viewModelUpdates() { MainActor.assumeIsolated { let vm = ViewModel() vm.update() #expect(vm.state == .updated) } } ``` ### Pattern 2: Legacy Delegate Callbacks From WWDC 2024-10169 — When documentation guarantees main thread delivery: ```swift @MainActor class LocationDelegate: NSObject, CLLocationManagerDelegate { var location: CLLocation? // CLLocationManager created on main thread delivers callbacks on main thread nonisolated func locationManager( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { MainActor.assumeIsolated { self.location = locations.last } } } ``` ### Pattern 3: @preconcurrency Shorthand `@preconcurrency` is equivalent shorthand — wraps in `assumeIsolated` automatically: ```swift // ❌ Manual approach (verbose) extension MyClass: SomeDelegate { nonisolated func callback() { MainActor.assumeIsolated { self.updateUI() } } } // ✅ Using @preconcurrency (equivalent, cleaner) extension MyClass: @preconcurrency SomeDelegate { func callback() { self.updateUI() // Compiler wraps in assumeIsolated } } ``` **When protocol adds isolation**: `@preconcurrency` becomes unnecessary and compiler warns. ### Pattern 4: Thread Check Before assumeIsolated When caller context is unknown (e.g., library code): ```swift func getView() -> UIView { if Thread.isMainThread { return createHostingViewOnMain() } else { return DispatchQueue.main.sync { createHostingViewOnMain() } } } private func createHostingViewOnMain() -> UIView { MainActor.assumeIsolated { let hosting = UIHostingController(rootView: MyView()) return hosting.view } } ``` ### Pattern 5: Custom Actor Access ```swift actor DataStore { var cache: [String: Data] = [:] nonisolated func synchronousRead(key: String) -> Data? { // Only safe if called from DataStore's executor assumeIsolated { isolated in isolated.cache[key] } } } ``` ## Common Mistakes ### Mistake 1: Silencing Compiler Errors ```swift // ❌ DANGEROUS: Using assumeIsolated to silence warnings func unknownContext() { MainActor.assumeIsolated { updateUI() // Crashes if not actually on main actor! } } // ✅ When uncertain, use proper async func unknownContext() async { await MainActor.run { updateUI() } } ``` ### Mistake 2: Assuming GCD Main Queue == MainActor They're **usually** the same, but not guaranteed. Check documentation or use async. ### Mistake 3: Using in Async Context ```swift // ❌ Unnecessary — you already have isolation @MainActor func updateState() async { MainActor.assumeIsolated { // Pointless self.state = .ready } } // ✅ Direct access @MainActor func updateState() async { self.state = .ready } ``` ## When @preconcurrency Becomes Unnecessary If the protocol later adds MainActor isolation: ```swift // Library update: @MainActor protocol CaffeineThresholdDelegate: AnyObject { func caffeineLevel(at level: Double) } // Your code — @preconcurrency now warns: // "@preconcurrency attribute on conformance has no effect" extension Recaffeinater: CaffeineThresholdDelegate { func caffeineLevel(at level: Double) { // Direct access, no wrapper needed } } ``` ## Crash Behavior Per Apple documentation: > "If the current context is not running on the actor's serial executor... this method will crash with a fatal error." **Trapping is intentional**: Better to crash than corrupt user data with a race condition. ## Resources **WWDC**: 2024-10169 **Docs**: /swift/mainactor/assumeisolated, /swift/actor/assumeisolated **Skills**: axiom-swift-concurrency