ObservableObject var not update on Text

110 views Asked by At

I want to update contentview Text when currentStatus change, but when run MLModel to load the model, it print 'loading resources...' but the currentStatus is not update the Text in ContentView,I try add DispatchQueue.main.async, but still not working

ContentView.swift

struct ContentView: View {
    @StateObject var model = Content.ViewModel()
    
    var body: some View {
        VStack {
            Text(model.currentStatus)
                .multilineTextAlignment(.center)
                .padding(.horizontal)
        }
        .padding()
        .onAppear {
            model.inited()
        }
    }
}

ContentModel.swift

class ContentModel: ObservableObject {
    @Published var currentStatus: String = "init..."
    func inited() {
        currentStatus = "loading resources..."
        print("currentStatus: \(self.currentStatus)")
        guard let path = Bundle.main.path(forResource: "Unet", ofType: "mlmodelc", inDirectory: "split") else {
            fatalError("Fatal error: failed to find the CoreML model.")
        }
        let resourceURL = URL(fileURLWithPath: path)
        let config = MLModelConfiguration()
        config.computeUnits = .cpuAndGPU
        
        var loadedModel: MLModel?
        do {
            loadedModel = try MLModel(contentsOf: resourceURL, configuration: config)
        } catch {
            print(error)
        }
        currentStatus = "complete"
        print("currentStatus: \(self.currentStatus)")
    }
}

Solution using asyncAfter, but the code look terrible @_@!!!

DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
            let startTime = Date()
            self.pipeline!.textEncoder.prewarmResources { result in
                switch result {
                case .success:
                    self.currentStatus =  String(format: "textEncoder took %.2f seconds.", Date().timeIntervalSince(startTime))
                case .failure(let error):
                    print("Error:", error)
                }
            }
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                let startTime = Date()
                self.pipeline!.unet.prewarmResources { result in
                    switch result {
                    case .success:
                        self.currentStatus =  String(format: "unet took %.2f seconds.", Date().timeIntervalSince(startTime))
                    case .failure(let error):
                        print("Error:", error)
                    }
                }
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                    let startTime = Date()
                    self.pipeline!.decoder.prewarmResources { result in
                        switch result {
                        case .success:
                            self.currentStatus =  String(format: "decoder took %.2f seconds.", Date().timeIntervalSince(startTime))
                        case .failure(let error):
                            print("Error:", error)
                        }
                    }
                }
            }
        }
3

There are 3 answers

1
workingdog support Ukraine On

Here is my test code, it updates the model currentStatus and display it in the Text(model.currentStatus) as it is changed.

import SwiftUI

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

class ContentModel: ObservableObject {
    @Published var currentStatus: String = "init..."
    
    func inited() {
        currentStatus = "loading resources..."  // <-- this is displayed
        print("----> currentStatus: \(self.currentStatus)")
        
        // simulated processing for 3 seconds
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            self.currentStatus = "complete"  // <-- this is displayed
            print("----> currentStatus: \(self.currentStatus)")
        }
    }
}

struct ContentView: View {
    @StateObject var model = ContentModel()
    
    var body: some View {
        VStack {
            Text(model.currentStatus) // <-- this changes as currentStatus changes
                .multilineTextAlignment(.center)
                .padding(.horizontal)
        }
        .padding()
        .onAppear {
            model.inited()
        }
    }
}
1
BobC On

It should be @StateObject, not @ObservedObject. And consider putting ContentModel definition into an extension of Model, so instead of having

 @ObservedObject var model = ContentModel()

you will have:

 @StateObject var model = Content.ViewModel()

I believe it makes the code more readable but YMMV.

0
jrturton On

Your question, and the answers, seem to be dealing with the wrong issue. Your problem is that you are attempting a long-running operation on the main queue, so the UI can't update because it is busy.

Your setup code should be marked as async or async throws and called from a task modifier, not an onAppear:

var body: some View {
        VStack {
            Text(model.currentStatus)
                .multilineTextAlignment(.center)
                .padding(.horizontal)
        }
        .padding()
        .task {
            await model.setup()
        }
    }

Then in your model class (which should be marked as @MainActor):

@MainActor
class ContentModel: ObservableObject {
    @Published var currentStatus: String = "init..."
    @Published var model: MLModel?

    func setup() async {
        currentStatus = "loading resources..."
        guard let path = Bundle.main.path(forResource: "Unet", ofType: "mlmodelc", inDirectory: "split") else {
            currentStatus = "Fatal error: failed to find the CoreML model."
        }
        let resourceURL = URL(fileURLWithPath: path)
        let config = MLModelConfiguration()
        config.computeUnits = .cpuAndGPU
        do {
            model = try await MLModel.load(contentsOf: resourceURL, configuration: config)
            currentStatus = "complete"
        } catch {
            currentStatus = "Error \(error.localizedDescription)"
        }
    }
}

Note that you should be using the async throwing class method of MLModel to load it asynchronously, rather than the direct initialiser, which assumes you are already working on a non-UI actor.