0

I want to make one-way binding from UIViewController to VewModel, in ReactiveObjC I've used RACChannel. What is the equivalent of a later one or what is the best way to do that?

DarkSatyr
  • 138
  • 4
  • Need more information. What is the problem you are trying to solve? Give more details please. – Daniel T. Mar 24 '22 at 10:28
  • In reading the ReactiveObjC documentation, I see that a RACChannel sets up a two-way binding so the fact that you say you want a one-way binding is a bit confusing. – Daniel T. Mar 24 '22 at 10:28

1 Answers1

0

I'm going to have to answer the more general question here. That of binding the view controller to the view model.

Here's a good video for Reactive View Models

A well constructed reactive view model is a single function that takes a number of Observable inputs from the view inputs and returns a number of Observables that are bound to the views meant for output.

A simple example login view model...

func login(
    callServer: @escaping (URLRequest) -> Observable<Data>,
    email: Observable<String>,
    password: Observable<String>,
    login: Observable<Void>
) -> (
    loggedIn: Observable<Void>,
    errorMessage: Observable<String>
) {
    let loginResponse = login
        .withLatestFrom(
            Observable.combineLatest(email, password) { makeLoginURLRequest(email: $0, password: $1) }
        )
        .flatMapLatest {
            callServer($0)
                .map { try JSONDecoder().decode(LoginResponse.self, from: $0) }
                .materialize()
        }
    
    let loggedIn = loginResponse
        .compactMap { $0.element != nil ? () : .none }
    
    let errorMessage = loginResponse
        .compactMap { $0.error?.localizedDescription }
    
    return (
        loggedIn: loggedIn,
        errorMessage: errorMessage
    )
}

Call it in the view controller's viewDidLoad like this:

let (loggedIn, errorMessage) = login(
    callServer: URLSession.shared.rx.data(request:),
    email: emailTextField.rx.text.orEmpty.asObservable(),
    password: passwordTextField.rx.text.orEmpty.asObservable(),
    login: loginButton.rx.tap.asObservable()
)

loggedIn
    .bind(onNext: { print("user is now logged in!") })
    .disposed(by: disposeBag)

errorMessage
    .bind(onNext: { print("display error message", $0) })
    .disposed(by: disposeBag)

Lastly, if you don't like using tuples, you can replace them with structs.

Daniel T.
  • 32,821
  • 6
  • 50
  • 72
  • I was using BehaviorRelay to push changes to ViewModel, data comes from multiple sources even from non-observable sources inside ViewController like method calls (for ex. setText: ) I was thinking of way to push just one direction events to ViewModel to eliminate cases that someone can push events to the same BehaviorRelay instance inside ViewModel – DarkSatyr Mar 24 '22 at 15:45
  • I suggest you push the changes directly from the inputs instead of using the relays, and replace the non-observable sources with observable ones. Simplify your life. Less code means fewer bugs. – Daniel T. Mar 24 '22 at 16:00
  • "replace the non-observable sources with observable ones" - but what if I have method like doA { } and inside it setText: argument of which I need to push to ViewModel? – DarkSatyr Mar 24 '22 at 16:04
  • Don't do that... I suggest you join the [RxSwift Slack](http://slack.rxswift.org) channel. – Daniel T. Mar 24 '22 at 19:39