Как привязать rx_tap (UIButton) к ViewModel?

У меня есть контроллер авторизации с 2 свойствами UITextField и 1 UIButton. Я хочу привязать свой View к ViewModel, но не знаю, как это сделать. Это мой 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?

}

}

И это мой 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)

Решение Вопроса

ViewModel вместе с двумя Observables из UITextFields.

Это небольшой рабочий пример для вашего сценария. (Я использовал небольшой клиентский класс аутентификации для имитации ответа от сервиса):

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

Это две интересные части:

// 1: мы вводим три наблюдаемых в ViewModel при инициализации.

// 2: Затем мы подписываемся на вывод ViewModel, чтобы получитьAuth модель после входа в систему.

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

ViewModel получает 3 Observables в своем методе init и подключает их к своему выходу:

// 1: Объединить самое последнее значение текстового поля логина и самое последнее значение текстового поля пароля в одно наблюдаемое.

// 2: Когда пользователь нажимает кнопку, используйте самое последнее значение текстового поля для входа в систему и самое последнее значение текстового поля для пароля и передайте его службе аутентификации, используяflatMap, Когда клиент аутентификации возвращаетAuthResponse, сопоставьте это сAuth модель. Установите результат этой «цепочки» какauthResponse выход изViewModel

 joern31 июл. 2016 г., 21:04
@DanielT. ты снова прав Кортеж здесь не имеет смысла. Я удалил их. Если ViewModel будет иметь больше параметров, которые не являются входными данными, можно сгруппировать их с кортежами, но в этом случае это не имеет смысла. Спасибо за ваш отзыв!
 Marina15 июл. 2016 г., 06:51
Спасибо большое! Мне было очень трудно разобраться, как это работает, и твой ответ действительно помог мне.
 Daniel T.18 июл. 2016 г., 03:01
Вам следует избегать использования тем, когда вы можете, и вы можете легко избежать этого в этом случае.
 joern01 авг. 2016 г., 15:45
@DanielT. Конечно, вы можете сделать это с помощью функции. Но 1) OP спросил об использовании ViewModel, 2) Я полагаю, что в классе ViewModel будет происходить больше (например, проверка, обработка забытого пароля и т. Д.), Тогда класс ViewModel будет подходящим вариантом. 3) Наличие этого кода в отдельном классе облегчает тестирование.
 Daniel T.01 авг. 2016 г., 15:49
@joern Для (1) ничто не говорит о том, что модель представления не может быть функцией, если это все, что нужно, а для (3) тестирование функции очень просто ... Вы просто вызываете функцию. Но у вас есть хорошая точка зрения с (2), как я упоминал в своем ответе, если есть несколько выходов, тогда лучше использовать класс, чем пытаться вернуть огромный кортеж. :-)
 Daniel T.31 июл. 2016 г., 20:18
Выглядит лучше. Я не уверен, почему вы помещаете аргументы конструктора в кортеж.
 Daniel T.01 авг. 2016 г., 15:05
@ Джерн Хорошо! Теперь последний комментарий. Обратите внимание, что ваш класс состоит только из одной функции (init) и одного свойства только для чтения, которое по сути является выходом для метода init. Это должно заставить вас задуматься, почему вы заключаете эту функцию в класс ... Почему бы просто не сделатьfunc authResponse(withLogin login: Observable<String>, password: Observable<String>, didPressButton: Observable<Void>) -> Observable<Auth>? Посмотрите на мой ответ на этот вопрос и подумайте об этом ...
 joern31 июл. 2016 г., 13:59
@DanielT Спасибо за ваш комментарий! Вы совершенно правы, я изменил пример в своем ответе, чтобы использовать способ, предложенный в репозитории RxSwift.

Ваш ответ на вопрос