Search code examples
swiftswiftuireactive-programmingswift5

Updating State from ObservableObject publisher


I was experimenting with Combine and SwiftUI but stuck in updating state basically I want to update my state in the view every time ObservableObject changes, here's the example.

class XViewModel: ObservableObject {
    @Published var tVal: Bool = false
    private var cancellables = Set<AnyCancellable>()

    func change() {
        Just(true)
            .delay(for: 3.0, scheduler: RunLoop.main)
            .receive(on: RunLoop.main)
            .assign(to: \.tVal, on: self)
            .store(in: &self.cancellables)
    }
}

I have viewModel and one publisher and delayed publisher which triggers after 3 seconds.

struct ContentView: View {
    @ObservedObject var viewModel = XViewModel()
    @State var toggleVal: Bool = false
    private var cancellables = Set<AnyCancellable>()

    init() {
        self.viewModel.$tVal
            .sink { newVal in
            print(newVal)
        }
        .store(in: &cancellables)        

        self.viewModel
            .$tVal.assign(to: \.toggleVal, on: self)
            .store(in: &cancellables)

        viewModel.change()
    }

    var body: some View {
        VStack {
            Toggle(isOn: self.$viewModel.tVal) {
                Text("Toggle")

            Toggle(isOn: self.$toggleVal) {
                Text("Toggle from View")
            }
    }

}

What I expected is

viewModel.Just triggers
viewModel.tVal publisher triggers
view.toggleVal state triggers 
updates UI 

But it seems although everything is updated it doesn't update state. Is there any way to update State or it wasn't meant to be updated at all and I need to bind my views directly to the viewModel's tVal value which is publisher.

Thanks.


Solution

  • The @State is not ready in init to operate, use instead .onReceive as below.

    struct ContentView: View {
        @ObservedObject var viewModel = XViewModel()
        @State var toggleVal: Bool = false
        private var cancellables = Set<AnyCancellable>()
    
        init() {
            self.viewModel.$tVal
                .sink { newVal in
                print(newVal)
            }
            .store(in: &cancellables)        
    
            viewModel.change()
        }
    
        var body: some View {
            VStack {
                Toggle(isOn: self.$viewModel.tVal) {
                    Text("Toggle")
    
                Toggle(isOn: self.$toggleVal) {
                    Text("Toggle from View")
                }
                .onReceive(self.viewModel.$tVal) { newVal in // << here !!
                    self.toggleVal = newVal
                }
        }
    
    }