1

How can I observe changes to Variable<...> value (RxSwift Variable) inside the ViewModel class from the ViewController?

So in case the value of any of my Variable<..> that I have in the ViewModel changes within the things happening in the ViewModel then the ViewController will be noticed "Hey! One or more Variable<..> in the ViewModel changed! Ask the ViewModel for the data you need to update the UI and update the UI!"

And then the ViewController call a method updateUI() inside the ViewController and within it it asks the ViewModel for all the info like status/state to update the UI something like:

func updateUI() {
  progressBar.hide = viewModel.getProgressBarVisibility()
  errorMessageLabel.hide = viewModel.getErrorMessageVisibility()
  errorMessageLabel.text = viewModel.getErrorMessageText()
  .....
  ...
}
Kamran
  • 14,987
  • 4
  • 33
  • 51
denis_lor
  • 6,212
  • 4
  • 31
  • 55
  • You can use `combineLatest` operator so that any change happens to any of your observables will trigger that, you can assign it's value to a property inside your viewModel and subscribe for it inside your viewController.. Also Variable is deprecated try to use `subjects` instead. – mojtaba al moussawi Nov 26 '18 at 11:36
  • 1
    As an FYI. Observables are a "tell don't ask" system. Your view controller should not "ask the ViewModel for the data". Instead the view controller _tells_ the variables in the ViewModel that they are interested in changes (through the subscribe function,) and then the variables _tell_ the view controller what the new values are as they are changed. And of course, `Variable` has been deprecated. – Daniel T. Nov 26 '18 at 19:47

3 Answers3

1

Why do you want to update the complete UI if the value of a single ViewModel's property changes?

RxSwift enables you to listen to changes independently and you can react/change UI accordingly.

In my view, this is how your ViewModel and ViewController classes should look:

class ViewModel {
    private var progressBarVisibiity:Variable<Double> = Variable.init(0.0)
    private var errorMessageVisibiity:Variable<Double> = Variable.init(0.0)
    private var errorMessageLabel:Variable<String> = Variable.init("Default text")

    public func setProgressBarVisibiity(_ value:Double) {
        progressBarVisibiity.value = value
    }

    public func setErrorMessageVisibiity(_ value:Double) {
        errorMessageVisibiity.value = value
    }

    public func setErrorMessageLabel(_ value:String) {
        errorMessageLabel.value = value
    }

    public func observeProgressBarVisibiity() -> Observable<Double> {
        return progressBarVisibiity.asObservable().observeOn(MainScheduler())
    }

    public func observeErrorMessageVisibiity() -> Observable<Double> {
        return errorMessageVisibiity.asObservable().observeOn(MainScheduler())
    }

    public func observeErrorMessageLabel() -> Observable<String> {
        return errorMessageLabel.asObservable().observeOn(MainScheduler())
    }
}

class ViewController {
    let viewModel = ViewModel()
    let disposeBag = DisposeBag()
    func observeViewModelChanges() {
        viewModel
            .observeProgressBarVisibiity()
            .subscribe(onNext: { value in
                self.progressBar.hide = viewModel.getProgressBarVisibility()
            })
            .disposed(by: disposeBag)

        viewModel
            .observeErrorMessageVisibiity()
            .subscribe(onNext: { value in
                self.errorMessageLabel.hide = value
            })
            .disposed(by: disposeBag)

        viewModel
            .observeErrorMessageLabel()
            .subscribe(onNext: { value in
                self.errorMessageLabel.text = value
            })
            .disposed(by: disposeBag)
    }
}
Puneet Sharma
  • 9,369
  • 1
  • 27
  • 33
  • I don't want to update the whole UI just to get the latest changes for the whole UI eventhough a single element changes. That because will make my ViewController sneller and I will be able to just get data from ViewModel and pass it to UI. Subscription and this kind of stuff can either happen in the ViewModel (for all the states) or it can happen in another layer something like ViewBinder – denis_lor Nov 26 '18 at 11:04
  • @denis_lor by subscribing to observables from viewmodel you get to know about independent changes in view model properties. That is what above code is doing. – Puneet Sharma Nov 26 '18 at 11:19
  • I have already implemented a solution that looks like yours, thats why I want to make something different. Your solution is good, but is not what I was looking for thanks anyway! – denis_lor Nov 26 '18 at 11:22
  • @denis_lor: You are looking for some kind of combining operator. merge is perfect for this case but can only be used with Observables of same kind. I guess you can use a common protocol between all Observable values and then listen to changes but that is not an ideal way to do this. You can look at selected answer of this question: https://stackoverflow.com/questions/39050059/rxswift-merge-different-kind-of-observables – Puneet Sharma Nov 26 '18 at 11:42
  • "Why do you want to update the complete UI if the value of a single ViewModel's property changes?" Because of simplicity. Splitting the filling of the views in smaller bits and pieces will open the code for bugs, make those bugs harder to find and have more code. It is fragmenting your code to the smallest possible level. – denis_lor Nov 28 '18 at 08:08
0

To update your UI I suggest to use a viewState variable that you can update, when needed, in your view model class, for example:

/// Making it generic allow you to add your view specific state
public enum ViewState<T> {
    // add all the case you need
    case loading
    case ready(T)
    case failure(Error)
}

Then in your viewModel class:

let viewState: Variable<ViewState<YourViewControllerState>> = Variable<ViewState<YourViewControllerState>>(.loading)

Where YourViewControllerState is an enum with your specific cases:

enum YourViewControllerState {
    case progressBarShowed, //...
}

And finally in your ViewController:

viewModel.viewState
    .asObservable()
    .observeOn(MainScheduler.instance)
    .subscribe { [weak self] _ in
        self?.updateUI()
    }.disposed(by: disposeBag)

private func updateUI() {
    guard isViewLoaded else {
        return
    }

    switch viewModel.viewState.value {
    case .ready(.progressBarShowed):
        progressBar.hide = viewModel.getProgressBarVisibility()

    case .failure:
        errorMessageLabel.hide = viewModel.getErrorMessageVisibility()
        errorMessageLabel.text = viewModel.getErrorMessageText()
    }
}
Francesco Deliro
  • 3,899
  • 2
  • 19
  • 24
  • I wanted to look for a different solution for my specific case. So I would like to check a way to trigger one type of event from the ViewModel everytime values in all the Variable<...> that are defined in the ViewModel changes. – denis_lor Nov 26 '18 at 11:14
0

We can construct a two-way binding operator which you can just use bindTo. Here are implementations for ControlProperty <-> Variable and Variable <-> Variable:

infix operator <->

@discardableResult func <-><T>(property: ControlProperty<T>, variable: BehaviorSubject<T>) -> Disposable {
    let variableToProperty = variable.asObservable()
        .bind(to: property)

    let propertyToVariable = property
        .subscribe(
            onNext: { variable.onNext($0) },
            onCompleted: { variableToProperty.dispose() }
    )

    return Disposables.create(variableToProperty, propertyToVariable)
}

You can find a detailed answer to your question in the following post. Two way binding in RxSwift

Suraj Rao
  • 29,388
  • 11
  • 94
  • 103