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.