Every time the user needs to compute the data of a persisted item, task that is expensive, I show a computing text while in background thread I compute the data and when is ready I show it to the user.
The app works as expected but
- I am not really sure 100% that my approach is the most efficient
- one warning appears:
Capture of 'self' with non-sendable type 'ItemEditView' in a
@Sendable
closure. Consider making struct 'ItemEditView' conform to the 'Sendable' protocol
struct ItemEditView: View {
@Environment(\.modelContext) private var modelContext
let item: Item
@State private var text = "Computing..."
var body: some View {
HStack{
Text(text)
}
.task{
Task.detached{
do {
let dataManager = DataManager(modelContainer: modelContext.container) //warning here!
try await dataManager.computeData(for: item.id)
text = String(item.data!)
// Not sure if saving the main context is also needed
try modelContext.save()
} catch {
print(error.localizedDescription)
}
}
}
}
}
@ModelActor
actor DataManager{
func computeData(for itemID: PersistentIdentifier) async throws {
let item = modelContext.model(for: itemID) as! Item
item.data = expensiveWork()
try modelContext.save()
}
}
@Model
final class Item {
var name: String
var data: Double?
init(name: String) {
self.name = name
}
}
struct ContentView: View {
@Query private var items: [Item]
@Environment(\.modelContext) private var modelContext
var body: some View {
NavigationView {
List {
ForEach(items) { item in
VStack(alignment: .leading){
Text("Name: \(item.name)")
if let data = item.data{
Text("Data: \(data)")
}
NavigationLink("Compute Data"){
ItemEditView(item: item)
}
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
}
}
private func addItem() {
withAnimation {
let newItem = Item(name: "Name\(items.count)")
modelContext.insert(newItem)
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
for index in offsets {
modelContext.delete(items[index])
}
}
}
}
Edit
extension DataManager {
private func expensiveWork() -> Double {
for i in 0..<100_000 {
print("\(i)")
}
return .random(in: 0...100)
}
}
To use a detached Task without getting any warnings about
Sendable
from the compiler we need to declare any local variables before the Task closure rather than inside, that way we won't captureself
.So we move the declaration of
dataManager
and we also need to declare a variable for model id or we get the same warningAnd for the same reason we can't update
text
inside the closure but we can fix that by running the task usingawait
and assigntext
afterwards(The code is from my test project so some properties and functions aren't named exactly the same but the logic and flow should match.)
If you want to run this every time a new object is shown in the view I would recommend using
task(id:)
instead oftask
I am not convinced you need to use
Task.detached
at all so here is my version without itAnd I simulated a long operation by using
try await Task.sleep(for: .seconds(2))
rather than afor
loop.