Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI - make sure to publish values from the main thread (via operators like receive(on:)) on model updates

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:

  1. User presses button that initiates the XML data pull that populates myArray
  2. myArray is populated on background thread, keeping UI responsive
  3. List in MyView automatically updates upon task completion. At present, I have to navigate away from the view and back again for the List to refresh.
like image 923
d5automations Avatar asked Dec 22 '22 18:12

d5automations


2 Answers

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

like image 130
Alexander Ryakhin Avatar answered Dec 25 '22 08:12

Alexander Ryakhin


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.

like image 23
Rob Napier Avatar answered Dec 25 '22 06:12

Rob Napier