Marina Marina - 4 months ago 133
Swift Question

How to bind rx_tap (UIButton) to ViewModel?

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

Answer

You can use a PublishSubject to bind the UIButton taps to your view model. It might also be easier if you use PublishSubjects for the UITextField values.

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 viewModel = ViewModel()
    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)

        setupBindings()
    }

    func setupBindings() {
        // 1
        loginTxtField.rx_text
            .bindTo(viewModel.login)
            .addDisposableTo(disposeBag)

        // 2
        passwordTxtField.rx_text
            .bindTo(viewModel.password)
            .addDisposableTo(disposeBag)

        // 3
        loginButton.rx_tap
            .bindTo(viewModel.didPressButton)
            .addDisposableTo(disposeBag)

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

There are 3 bindings and 1 subscription happening in the ViewController:

// 1: The text value of the login text field is bound to the ViewModel's login input

// 2: The text value of the password text field is bound to the ViewModel's password input

// 3: The tap event of the button is bound to the ViewModel's didPressButton input (The main point of your question)

// 4: Finally we subscribe to the ViewModel's authResponse 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 {

    // Input
    let login = PublishSubject<String>()
    let password = PublishSubject<String>()
    let didPressButton = PublishSubject<Void>()

    // Output
    var authResponse: Observable<Auth> = Observable.never()

    // Private
    let mockAuthService = MockAuthService()

    init() {
        setup()
    }

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

       // 2
       authResponse = didPressButton
            .withLatestFrom(userInputs)
            .flatMap { [unowned self] (login, password) in
                return self.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)
    }
}

In setup() you connect the three inputs to the viewModels output. Then you can subscribe to the output in your view controller. This is how the connection is made:

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

Comments