sam-w
sam-w

Reputation: 7687

How can I update view state in response to external changes?

Imagine I have a view with some mutable state, but that the state might need to be updated to reflect changes in another object (e.g. a ViewModel).

How can I implement that in SwiftUI?

I've tried the following, but can't get the view to reflect updates coming from the ViewModel:

class ViewModel: ObservableObject {

    @Published var text: String = "loading"

    private var task: AnyCancellable?

    func fetch() {
        task = Just("done")
            .delay(for: 1, scheduler: RunLoop.main)
            .assign(to: \.text, on: self)
    }
}

struct ContentView: View {

    @ObservedObject var viewModel = ViewModel()

    @State var viewText = "idle"

    private var bind: AnyCancellable?

    init() {
        viewText = viewModel.text
        bind = viewModel
            .$text
            .print()
            .assign(to: \.viewText, on: self)
    }

    var body: some View {
        VStack {
            TextField(titleKey: "editable text", text: $viewText)
            Text(viewText)
            Text(viewModel.text)
        }
        .onAppear {
            self.viewModel.fetch()
        }
    }
}

The TextField and the first Text element get their content from ContentView.viewText, the second Text goes directly to the source: ViewModel.text.

As expected, the second Text shows "loading" and then "done". The first Text never changes from "idle".

Upvotes: 3

Views: 741

Answers (2)

user3441734
user3441734

Reputation: 17534

If next screen recording looks like answering your question

enter image description here

it was recorded using next code

import SwiftUI
import Combine

class ViewModel: ObservableObject {

    @Published var text: String = "loading"

    private var task: AnyCancellable?

    func fetch() {
        task = Just("done")
            .delay(for: 3, scheduler: RunLoop.main)
            .assign(to: \.text, on: self)
    }
}

struct ContentView: View {

    @ObservedObject var viewModel = ViewModel()
    @State var viewText = "idle"

    var body: some View {
        VStack {
            Text(viewText)
            Text(viewModel.text)
        }.onReceive(viewModel.$text.filter({ (s) -> Bool in
            s == "done"
        })) { (txt) in
                self.viewText = txt
        }.onAppear {
            self.viewModel.fetch()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Upvotes: 3

Asperi
Asperi

Reputation: 257493

Here is possible approach (tested & works with Xcode 11.2 / iOS 13.2) - modified only ContentView:

struct ContentView: View {

    @ObservedObject var viewModel = ViewModelX()
    @State private var viewText = "idle"

    init() {
        _viewText = State<String>(initialValue: viewModel.text)
    }

    var body: some View {
        VStack {
            Text(viewText)
            Text(viewModel.text)
        }
        .onReceive(viewModel.$text) { value in
            self.viewText = value
        }
        .onAppear {
            self.viewModel.fetch()
        }
    }
}

Upvotes: 2

Related Questions