15

I have authorization controller with 2 UITextField properties and 1 UIButton. I want to bind my View to ViewModel but don't know how to to do it. This is my AuthorizatioVC.swift:

class AuthorizationViewController: UIViewController {

let disposeBag = DisposeBag()

@IBOutlet weak var passwordTxtField: UITextField!
@IBOutlet weak var loginTxtField: UITextField!

@IBOutlet weak var button: UIButton!

override func viewDidLoad() {
    super.viewDidLoad()

    addBindsToViewModel()

}

func addBindsToViewModel(){
    let authModel = AuthorizationViewModel(authClient: AuthClient())

    authModel.login.asObservable().bindTo(passwordTxtField.rx_text).addDisposableTo(self.disposeBag)
    authModel.password.asObservable().bindTo(loginTxtField.rx_text).addDisposableTo(self.disposeBag)
  //HOW TO BIND button.rx_tap here?

}

}

And this is my AuthorizationViewModel.swift:

final class AuthorizationViewModel{


private let disposeBag = DisposeBag()

//input
//HOW TO DEFINE THE PROPERTY WHICH WILL BE BINDED TO RX_TAP FROM THE BUTTON IN VIEW???
let authEvent = ???
let login = Variable<String>("")
let password = Variable<String>("")

//output
private let authModel: Observable<Auth>

init(authClient: AuthClient){

   let authModel = authEvent.asObservable()
            .flatMap({ (v) -> Observable<Auth> in
                    return authClient.authObservable(String(self.login.value), mergedHash: String(self.password.value))
                        .map({ (authResponse) -> Auth in
                            return self.convertAuthResponseToAuthModel(authResponse)
                        })
              })
}


func convertAuthResponseToAuthModel(authResponse: AuthResponse) -> Auth{
    var authModel = Auth()
    authModel.token = authResponse.token
    return authModel
}
}
Marina
  • 1,177
  • 4
  • 14
  • 27

3 Answers3

15

You can turn the taps on the UIButton into an Observable and hand it to the ViewModel along with the two Observables from the UITextFields.

This is a small working example for your scenario. (I used a small auth client mock class to simulate the response from the service):

The ViewController:

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {

    let loginTxtField = UITextField(frame: CGRect(x: 20, y: 50, width: 200, height: 40))
    let passwordTxtField = UITextField(frame: CGRect(x: 20, y: 110, width: 200, height: 40))
    let loginButton = UIButton(type: .RoundedRect)

    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1)

        loginTxtField.backgroundColor = UIColor.whiteColor()
        view.addSubview(loginTxtField)

        passwordTxtField.backgroundColor = UIColor.whiteColor()
        view.addSubview(passwordTxtField)

        loginButton.setTitle("Login", forState: .Normal)
        loginButton.backgroundColor = UIColor.whiteColor()
        loginButton.frame = CGRect(x: 20, y: 200, width: 200, height: 40)
        view.addSubview(loginButton)

        // 1
        let viewModel = ViewModel(
            withLogin: loginTxtField.rx_text.asObservable(),
            password: passwordTxtField.rx_text.asObservable(),
            didPressButton: loginButton.rx_tap.asObservable()
        )

        // 2
        viewModel.authResponse
            .subscribeNext { response in
                print(response)
            }
            .addDisposableTo(disposeBag)
    }
}

These are the two interesting parts:

// 1: We inject the three Observables into the ViewModel when we initialize it.

// 2: Then we subscribe to the ViewModel's output to receive the Auth model after the login was done.

The ViewModel:

import RxSwift

struct Auth {
    let token: String
}

struct AuthResponse {
    let token: String
}

class ViewModel {

    // Output
    let authResponse: Observable<Auth>

    init(withLogin login: Observable<String>, password: Observable<String>, didPressButton: Observable<Void>) {
        let mockAuthService = MockAuthService()

        // 1
        let userInputs = Observable.combineLatest(login, password) { (login, password) -> (String, String) in
            return (login, password)
        }

        // 2
        authResponse = didPressButton
            .withLatestFrom(userInputs)
            .flatMap { (login, password) in
                return mockAuthService.getAuthToken(withLogin: login, mergedHash: password)
            }
            .map { authResponse in
                return Auth(token: authResponse.token)
            }
    }
}

class MockAuthService {
    func getAuthToken(withLogin login: String, mergedHash: String) -> Observable<AuthResponse> {
        let dummyAuthResponse = AuthResponse(token: "dummyToken->login:\(login), password:\(mergedHash)")
        return Observable.just(dummyAuthResponse)
    }
}

The ViewModel gets the 3 Observables in its init method and connects them to its output:

// 1: Combine the latest value of the login text field and the latest value of the password text field into one Observable.

// 2: When the user presses the button, use the latest value of the login text field and the latest value of the password text field and pass that to the auth service using flatMap. When the auth client returns a AuthResponse, map that to the Auth model. Set the result of this "chain" as the authResponse output of the ViewModel

joern
  • 27,354
  • 7
  • 90
  • 105
  • Thank u so much! I had a really hard time trying to sort out how it works and your answer really helped me. – Marina Jul 15 '16 at 04:51
  • You should avoid using Subjects when you can, and you can avoid it easily in this case. – Daniel T. Jul 18 '16 at 01:01
  • @DanielT Thanks for you commment! You are totally right, I changed the example in my answer to use the way as it is suggested in the RxSwift repo. – joern Jul 31 '16 at 11:59
  • Looks better. I'm not sure why you put the constructor arguments in a tuple though. – Daniel T. Jul 31 '16 at 18:18
  • @DanielT. you're right again. The tuple does not make sense here. I removed them. If the ViewModel would have more parameters that are not inputs one might group them with tuples, but in this case it does not make sense. Thanks for your feedback! – joern Jul 31 '16 at 19:04
  • @joern Nice! Now one last comment. Notice how your class consists of only one function (the init) and one read-only property which is essentially the output for the init method. That should make you wonder why you are wrapping this function in a class... Why not just do `func authResponse(withLogin login: Observable, password: Observable, didPressButton: Observable) -> Observable`? Look at my answer to this question and think about it... – Daniel T. Aug 01 '16 at 13:05
  • @DanielT. Sure, you could do this with a function. But 1) OP asked about using a ViewModel, 2) I guess there will be more happening in the ViewModel class later (like verification, forgot password handling etc.), then a ViewModel class would be be the way to go. 3) Having this code in a separate class makes testing easier. – joern Aug 01 '16 at 13:45
  • @joern For (1) there is nothing saying that a view model can't be a function if that's all that's needed and for (3) testing a function is very easy... You just call the function. But you have a good point with (2), as I mentioned in my answer, if there are multiple outputs then using a class is better than trying to return a huge tuple. :-) – Daniel T. Aug 01 '16 at 13:49
  • @joern I know that it's 4 years too late and RxSwift has evolved much, but I have some questions: 1. You set the observables from the view controller in the init. Won't this be a bad idea if you are using Dependency Injection library such as Typhoon or Swinject? 2. Won't the authResponse be unusable if it returned some kind of errors? – Bawenang Rukmoko Pardian Putra Dec 07 '20 at 14:44
  • 1
    @BawenangRukmokoPardianPutra Thanks for your questions. Regarding your questions: 1) Yes, if you want to use dependency injection you wouldn’t do that in the initializer. 2) Correct, in the real world you would have to add error handling. – joern Dec 07 '20 at 14:50
  • Thanks for answering pretty fast. I didn't think I will get a reply so soon. About question number two, it was actually from my own personal experience. I had a bug in my app when the API returned an error, the whole scene is unusable because the observable is disposed. It was my fault though because I didn't add a unit test because it didn't cross my mind. I fixed it by using `materialize()` to transform them into events. – Bawenang Rukmoko Pardian Putra Dec 07 '20 at 15:10
  • Also, just IMHO, but using SIgnals and Drivers is a much better approach for me. But, those aren't available yet in 2016. So it's understandable. – Bawenang Rukmoko Pardian Putra Dec 07 '20 at 15:13
8

First approach use PublishSubject

class ViewController: UIViewController {
  @IBOutlet weak var loginBtn: UIButton!
  var vm: ViewModel?
  let disposebag = DisposeBag()

  override func viewDidLoad() {
      super.viewDidLoad()
      bindUi()
  }

  func bindUi() {
    (loginBtn.rx.tap).bind(to: vm!.loginSbj).addDisposableTo(disposebag)
  }
}

class ViewModel {
  let loginSbj = PublishSubject<Void>()

  init() {
    loginSbj.do(onNext: { _ in
      // do something
    })
  }

}

The second approach use Action

class ViewController: UIViewController {
   @IBOutlet weak var loginBtn: UIButton!
   var vm: ViewModel?

   override func viewDidLoad() {
       super.viewDidLoad()
       bindUi()
   }

   func bindUi() {
       loginBtn.rx.action = vm!.loginAction
   }
}

class ViewModel {

  let loginAction: CococaAction<Void, Void> = CocoaAction {
    // do something
  }
}
user3165616
  • 113
  • 3
  • 8
4

The problem here is that you are trying to make your "viewModel" a class. It should be a function.

func viewModel(username: Observable<String>, password: Observable<String>, button: Observable<Void>) -> Observable<Auth> {
    return button
        .withLatestFrom(Observable.combineLatest(login, password) { (login, password) })
        .flatMap { login, password in
            server.getAuthToken(withLogin: login, password: password)
        }
        .map { Auth(token: $0.token) }

Use set it up by doing this in your viewDidLoad:

let auth = viewModel(loginTxtField.rx_text, passwordTxtField.rx_text, button.rx_tap)

If you have multiple outputs for your view model, then it might be worth it to make a class (rather than returning a tuple from a function.) If you want to do that, then GithubSignupViewModel1 from the examples in the RxSwift repo is an excellent example of how to set it up.

Daniel T.
  • 32,821
  • 6
  • 50
  • 72