Last active
September 6, 2024 16:12
-
-
Save benrudhart/a4bacb80807ef0eebec0ca83a0e683f3 to your computer and use it in GitHub Desktop.
Example for `nonisolated` keyword usage
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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