Nonisolated Methods
Swift concurrency annotation that allows methods within actor-isolated contexts to execute without actor isolation, enabling safe access from multiple threads without requiring async calls or actor hopping.
Core Concept
When a class is marked with @MainActor, all its methods are isolated to the main thread by default. The nonisolated keyword creates exceptions that can be called from any thread without async/await.
@MainActor
class AudioController {
private let buffer = AudioBuffer()
// Runs on main thread, requires await from other contexts
func updateUI() {
// UI updates here
}
// Can be called from any thread without await
nonisolated func processAudio(_ samples: [Float]) {
// Cannot access main-actor-isolated properties here
processOnBackgroundThread(samples)
}
}
Access Restrictions
Isolated Property Access: Nonisolated methods cannot directly access actor-isolated stored properties or call isolated methods:
@MainActor
class Controller {
private var state: String = ""
nonisolated func background() {
// ❌ Error: cannot access main-actor-isolated property
print(state)
// ✅ OK: can access through async call
Task { @MainActor in
print(state)
}
}
}
Safe Access Patterns: Use immutable data, computed properties, or async closures for safe access:
@MainActor
class Controller {
private let config: Config = Config() // immutable, OK to access
nonisolated func process() {
// ✅ Safe: immutable property
let setting = config.defaultSetting
// ✅ Safe: async main actor access
Task { @MainActor in
self.updateState(with: result)
}
}
}
Common Use Cases
Audio Processing Callbacks
AVAudioEngine callbacks need to execute on audio threads, not main thread:
@MainActor
class WakeWordDetector {
private var isListening = false
func startListening() {
audioEngine.inputNode.installTap(on: 0, bufferSize: 1024, format: format) { [weak self] buffer, _ in
self?.processAudioBuffer(buffer)
}
}
nonisolated private func processAudioBuffer(_ buffer: AVAudioPCMBuffer) {
// Safe to call from audio thread
let samples = extractSamples(from: buffer)
wakeWordQueue.async {
self.runInference(samples)
}
}
}
Worker Queue Integration
Interface between main-actor-isolated controllers and background workers:
@MainActor
class Manager {
private let worker = BackgroundWorker()
nonisolated func enqueueWork(_ data: Data) {
// Called from various threads, dispatches to worker
worker.process(data) { [weak self] result in
Task { @MainActor in
self?.handleResult(result)
}
}
}
}
Thread-Safe State Access
Enable safe access to immutable or carefully managed state:
@MainActor
class StateManager {
private var _counter: Int = 0
private let counterQueue = DispatchQueue(label: "counter")
nonisolated func incrementCounter() {
counterQueue.async {
self._counter += 1
}
}
nonisolated func getCounter() -> Int {
counterQueue.sync { _counter }
}
}
Unsafe Patterns
nonisolated(unsafe): More dangerous variant that bypasses safety checks entirely:
@MainActor
class UnsafeController {
private var dangerousState: String = ""
nonisolated(unsafe) func directAccess() {
// ⚠️ Compiler allows this but creates race conditions
dangerousState = "modified from any thread"
}
}
Use nonisolated(unsafe) only when you've manually verified thread safety through other means (locks, atomic operations, etc.).
Best Practices
Immutable Access Only: Prefer accessing only immutable properties or constants from nonisolated methods.
Async Callbacks: Use Task { @MainActor in } to safely access isolated state from nonisolated contexts.
Worker Delegation: Delegate heavy work to separate classes that don't need actor isolation.
Clear Threading Model: Document which methods can be called from which threads, especially in audio processing contexts.
Avoid Shared Mutable State: Design APIs to minimize shared mutable state between isolated and nonisolated contexts.