Reputation: 559
My app contains a resource heavy operation that populates an Array based on data pulled from an XML feed. I do not want this operation to lock up the main thread (and the UI when the array is given new data), so it's done in the background.
let dispatchQueue = DispatchQueue(label: "concurrent.queue", qos: .utility, attributes: .concurrent)
class XMLHandler: ObservableObject {
let context: NSManagedObjectContext
@Published var myArray: [CustomObject] = []
init(context: NSManagedObjectContext) {
self.context = context
}
...some code...
func populateArray {
dispatchQueue.async {
...xml parsing happens...
(xmlOutputObject) in
for x in xmlOutputObject {
self.myArray.append(x)
}
}
}
}
Elsewhere, my SwiftUI View uses myArray to populate it's List:
struct MyView: View {
@EnvironmentObject var handler: XMLHandler
var body: some View {
List{
ForEach(handler.myArray) { CustomObject in
... generate rows ...
}
}
}
My error on runtime occurs when my app tries to update @Published var myArray: [CustomObject] = [].
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.
I know this is something to do with adopting Combine, but I honestly have no idea where to start. Any help would be appreciated.
I simply want the following to happen:
Upvotes: 40
Views: 49412
Reputation: 590
In situations where the @MainActor
attribute doesn't work for you, it might be due to updating the UI from completion handlers executed on a background thread.
For instance, when using biometric authentication like Face ID or Touch ID and calling evaluatePolicy()
to authenticate the user, the completion handler may execute on a background thread despite the method being marked as @MainActor
. In such cases, using @MainActor
alone might not work because the completion handler executes on the background thread.
To solve this, you can use a Task
executed directly on the @MainActor
, eliminating the need for DispatchQueue.main.async{}
.
Task { @MainActor in
// Update the @Published property here
self.title = "Evaluation successful"
}
This approach ensures that the update to the @Published
property occurs on the main thread, even when handling tasks or completion handlers executed on a background thread.
Upvotes: 7
Reputation: 1510
fetchedMovies is object Which values you get from API response.
movies is @Published variable which use to updates UI of View class
await MainActor.run {
movies = fetchedMovies
}
Upvotes: 5
Reputation: 26652
iOS expects all UI changes to be on the main thread. SwiftUI is no exception. It is acceptable to perform UI related work off the main thread but sooner or later those changes need to be brought onto the main thread to be rendered.
It doesn’t seem that you are using Combine. However, if you were then this error;
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.
is best solved using the suggested Combine scheduling operator:
.receive(on: DispatchQueue.main)
This is detailed in Move Work Between Dispatch Queues With Scheduling Operators in Apple’s Processing URL Session Data Task Results with Combine article:
cancellable = urlSession
.dataTaskPublisher(for: url)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { print ("Received completion: \($0).") },
receiveValue: { print ("Received data: \($0.data).")})
Upvotes: 14
Reputation: 651
DispatchQueue.main.async {
nc.post(name: Notification.Name(NotificationStrings.AccountPostReceived), object: nil)
}
Upvotes: 14
Reputation: 569
Just put @MainActor
before defining your class that is supposed to act in the main thread. And it's so simple in terms of using new Swift concurrency.
@MainActor class DocumentsViewModel: ObservableObject { ... }
There's a lot of information about that in new WWDC 2021 videos or articles like that: Using the MainActor attribute to automatically dispatch UI updates on the main queue
Upvotes: 56
Reputation: 299345
Since the appending happens in a loop, you need to decide if you want to emit a new value once per item, or once for the whole update. If you're not certain, update it once for the whole update.
Then perform that action on the main queue:
...xml parsing happens...
(xmlOutputObject) in
DispatchQueue.main.async { // <====
self.append(contentsOf: xmlOutputObject)
}
The key point is that you cannot read or write properties on multiple queues. And in this case the queue you want to use is the main one (because it's driving the UI). So you must make sure that all property access happen on that queue.
Upvotes: 21