I have an ObservableObject
class and a SwiftUI view. When a button is tapped, I create a Task
and call populate
(an async function) from within it. I thought this would execute populate
on a background thread but instead the entire UI freezes. Here's my code:
class ViewModel: ObservableObject {
@Published var items = [String]()
func populate() async {
var items = [String]()
for i in 0 ..< 4_000_000 { /// this usually takes a couple seconds
items.append("\(i)")
}
self.items = items
}
}
struct ContentView: View {
@StateObject var model = ViewModel()
@State var rotation = CGFloat(0)
var body: some View {
Button {
Task {
await model.populate()
}
} label: {
Color.blue
.frame(width: 300, height: 80)
.overlay(
Text("\(model.items.count)")
.foregroundColor(.white)
)
.rotationEffect(.degrees(rotation))
}
.onAppear { /// should be a continuous rotation effect
withAnimation(.easeInOut(duration: 2).repeatForever()) {
rotation = 90
}
}
}
}
Result:
The button stops moving, then suddenly snaps back when populate
finishes.
Weirdly, if I move the Task
into populate
itself and get rid of the async
, the rotation animation doesn't stutter so I think the loop actually got executed in the background. However I now get a Publishing changes from background threads is not allowed
warning.
func populate() {
Task {
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
self.items = items /// Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
}
}
/// ...
Button {
model.populate()
}
Result:
How can I ensure my code gets executed on a background thread? I think this might have something to do with MainActor
but I'm not sure.
await
means that you wait for theasync
function to return you a result (which obviously blocks the current thread, which is main). In your second option you run the task on a background thread, so you need to ensure that results of the task are delivered to the main thread. The error you see hints you about this and provides an example, how to achieve this. How do you deliver items to the UI? Is it just a property on an observable object? – LassiterTask
s asynchronous? When Iawait
inside aTask
shouldn't the current thread be a background thread? I can't figure out the difference between the first and second options. Anyway in SwiftUI all you need to do is set the property and the UI automatically updates. – ThapsusMainActor.run { self.items = items }
to move the update to the main thread? – LassiterReference to captured var 'items' in concurrently-executing code
– Thapsus