Advanced Concurrency

nonisolated and Actor Boundaries

January 19, 2026
5 min read
Featured image for blog post: 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."

Originally published on deadpixel.llc

© 2026 Christopher Moore / Dead Pixel Studio

Let's work together

Professional discovery, design, and complete technical coverage for your ideas

Get in touch