How to bind rx_tap (UIButton) to ViewModel?

2020-06-03 04:46发布

问题:

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
}
}

回答1:

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



回答2:

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
  }
}


回答3:

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.