Skip to content

Instantly share code, notes, and snippets.

@benrudhart
Last active September 6, 2024 16:12
Show Gist options
  • Save benrudhart/a4bacb80807ef0eebec0ca83a0e683f3 to your computer and use it in GitHub Desktop.
Save benrudhart/a4bacb80807ef0eebec0ca83a0e683f3 to your computer and use it in GitHub Desktop.
Example for `nonisolated` keyword usage
import SwiftUI
@MainActor // (default as of iOS 18)
struct MyView: View {
let ascending = true
@State var sortedData: [Int] = []
let dataRepository: MyDataRepository
var body: some View {
List(sortedData, id: \.self) { value in
Text(value, format: .number)
}
// Setting a `.background` priority might look like what one is looking for but is misleading!
.task(priority: .background) {
await updateData_blocking()
}
.task {
await updateData_nonBlocking()
}
.task {
await updateData_nonBlockingBetter()
}
}
private func updateData_blocking() async {
// ✅ `fetchData` is not bound to @MainActor (depends on the implementation)
let data = await dataRepository.fetchData()
// ❌ here we're back on the main actor.
// Don't do anything that might block your UI, e.g. sorting, filtering, mapping or even parsing of data! (all architectural discussions aside, please)
@State var sortedData: [Int] = []
sortedData = data.sorted(by: { ascending ? $0 < $1 : $0 > $1 })
}
nonisolated private func updateData_nonBlocking() async {
// due to the `nonisolated` keyword this function is no longer isolated to the MainActor (of the view)
// hence we're on any other (not guaranteed) actor, but surely not on the Main Thread because MainActor is tied to the Main Thread
let data = await dataRepository.fetchData()
// ✅ we're not on the main actor (i.e. not main thread)
assert(!Thread.isMainThread, "Must not be executed on the main thread")
let sortedData = data.sorted(by: { ascending ? $0 < $1 : $0 > $1 })
// since we're not on the main actor we need to dispatch to it
// see below for a better/ more beautiful solution
await MainActor.run {
self.sortedData = sortedData
}
}
private func updateData_nonBlockingBetter() async {
// due to the `nonisolated` keyword this function is no longer isolated to the MainActor (of the view)
// hence we're on any other (not guaranteed) actor, but surely not on the Main Thread because MainActor is tied to the Main Thread
// ✅ `fetchData` is not bound to @MainActor (depends on the implementation)
let data = await dataRepository.fetchData()
// ✅ We're still on the MainActor here, hence we can simply assign self.sortedData
sortedData = await sortData(data: data)
}
/// ✅ adding nonisolated will ensure this function is not run on the actor of the view (the MainActor)
nonisolated private func sortData(data: [Int]) async -> [Int] {
assert(!Thread.isMainThread, "Must not be executed on the main thread")
return data.sorted(by: { ascending ? $0 < $1 : $0 > $1 })
}
}
final class MyDataRepository: Sendable {
/// Performs some networking or local DB query.
/// Just an example, in reality data might be way more complex and heavy, e.g. bigger model
func fetchData() async -> [Int] {
// Just mock some data here
(0..<1000).reduce(into: []) { array, _ in
array.append(Int.random(in: 1...100))
}
}
}
import Foundation
import SwiftData
/// - important: Any SwiftData @ModelActor will perform all work on the thread/ actor which is used to initialize ModelActor!!!
/// We need to get off the MainActor in order to create the ModelActor for it to operate on a non-MainActor
final class MyViewModel {
let modelContainer: ModelContainer
init(modelContainer: ModelContainer) {
self.modelContainer = modelContainer
}
func fetchModelCount_solution1() async -> Int {
// ❌ since the VM is @MainActor we're on the MainActor here
// See warning above: we need to get off the MainActor to create the ModelActor
let task = Task.detached {
// ✅ we're no longer on the main actor
let dataActor = MyDataModelActor(modelContainer: self.modelContainer)
return await dataActor.fetchModelCount()
}
// ⚠️ This solution works but we're losing structure concurrency by using `Task.detached`, i.e. we lose forwarding of task cancellation
return await task.value
}
nonisolated func fetchModelCount_solution2() async -> Int {
// ✅ by using `nonisolated` we make sure this is not executed on the MainActor
// We're still using structured concurrency
let dataActor = MyDataModelActor(modelContainer: modelContainer)
return await dataActor.fetchModelCount()
}
}
@ModelActor
actor MyDataModelActor {
func fetchModelCount() async -> Int {
assert(!Thread.isMainThread, "Must not be executed on the MainActor, check how/ where self is initialized")
// Here we would do something like this: modelContext.fetchCount(<YourFetchDescriptorHere>)
// (or any other SwiftData fetch operation)
// Just mock some data here
return .random(in: 0...10000)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment