Skillshub axiom-assume-isolated
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.
git clone https://github.com/ComeOnOliver/skillshub
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/CharlesWiltgen/Axiom/axiom-assume-isolated" ~/.claude/skills/comeonoliver-skillshub-axiom-assume-isolated && rm -rf "$T"
skills/CharlesWiltgen/Axiom/axiom-assume-isolated/SKILL.mdassumeIsolated — 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
instead)await - Already in async context (you have isolation)
- Cross-actor calls needed (use async)
- Callback origin is unknown or untrusted
API Reference
MainActor.assumeIsolated
static func assumeIsolated<T>( _ 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
func assumeIsolated<T>( _ operation: (isolated Self) throws -> T, file: StaticString = #fileID, line: UInt = #line ) rethrows -> T where T: Sendable
Task vs assumeIsolated
| Aspect | | |
|---|---|---|
| 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
@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:
@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:
// ❌ 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):
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
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
// ❌ 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
// ❌ 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:
// 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