nonisolated and Actor Boundaries

When your @MainActor class needs to do background work, nonisolated lets specific methods escape the main thread.
Follow along with the code: iOS-Practice on GitHub
The Problem
Everything in a @MainActor class runs on main thread by default:
@MainActor
class DataProcessor: ObservableObject {
@Published var result: Data?
func process(_ input: Data) -> Data {
// This runs on main thread - blocks UI!
return heavyComputation(input)
}
}
nonisolated to the Rescue
Mark methods that don't touch actor state:
@MainActor
class DataProcessor: ObservableObject {
@Published var result: Data?
nonisolated func process(_ input: Data) -> Data {
// Free from main actor - can run anywhere
return heavyComputation(input)
}
func processAndStore(_ input: Data) async {
let processed = await Task.detached {
self.process(input) // Runs on background
}.value
result = processed // Back on main for @Published
}
}
Rules for nonisolated
nonisolated methods CANNOT access mutable actor state:
@MainActor
class Counter: ObservableObject {
@Published var count = 0
nonisolated func getCount() -> Int {
count // ❌ Error: can't access actor state
}
nonisolated func compute(_ value: Int) -> Int {
value * 2 // ✅ OK: doesn't touch state
}
}
Accessing Actor State from nonisolated
Use await to hop back:
@MainActor
class UserService: ObservableObject {
@Published var users: [User] = []
nonisolated func fetchAndProcess() async {
// Do heavy work off main thread
let rawData = await api.fetchUsers()
let processed = processData(rawData) // Heavy computation
// Hop to main actor to update state
await MainActor.run {
// ❌ Can't access self.users directly in nonisolated
}
}
// Better pattern: separate concerns
nonisolated func processUsers(_ data: Data) -> [User] {
// Pure function, no state access
return parse(data)
}
func loadUsers() async {
let data = await api.fetchRawData()
let users = await Task.detached {
self.processUsers(data)
}.value
self.users = users // On main actor
}
}
nonisolated(unsafe)
For advanced cases where YOU guarantee thread safety:
@MainActor
class Cache: ObservableObject {
// Thread-safe via internal synchronization
nonisolated(unsafe) let storage = NSCache<NSString, UIImage>()
nonisolated func cachedImage(for key: String) -> UIImage? {
storage.object(forKey: key as NSString)
}
}
Use sparingly—you're promising the compiler something it can't verify.
Computed Properties
Can also be nonisolated:
@MainActor
class Settings: ObservableObject {
@Published var items: [Item] = []
nonisolated var isEmpty: Bool {
// ❌ Error: accesses actor state
items.isEmpty
}
// For read-only derived values, use methods instead
func checkEmpty() -> Bool {
items.isEmpty // ✅ On main actor
}
}
Complete Example: Image Processing
@MainActor
class ImageEditor: ObservableObject {
@Published var originalImage: UIImage?
@Published var editedImage: UIImage?
@Published var isProcessing = false
// Heavy work - keep off main thread
nonisolated func applyFilter(_ image: UIImage, filter: CIFilter) -> UIImage? {
guard let ciImage = CIImage(image: image) else { return nil }
filter.setValue(ciImage, forKey: kCIInputImageKey)
guard let output = filter.outputImage else { return nil }
let context = CIContext()
guard let cgImage = context.createCGImage(output, from: output.extent) else {
return nil
}
return UIImage(cgImage: cgImage)
}
func applySepia() async {
guard let image = originalImage else { return }
isProcessing = true
let filter = CIFilter(name: "CISepiaTone")!
filter.setValue(0.8, forKey: kCIInputIntensityKey)
// Process in background
let result = await Task.detached(priority: .userInitiated) {
self.applyFilter(image, filter: filter)
}.value
editedImage = result
isProcessing = false
}
}
Interview Tip
Explain the trade-off: "I use nonisolated to move heavy computation off the main thread while keeping my ViewModel's state updates on main actor. The key is understanding what accesses actor state and what's a pure computation."