web-dev-qa-db-fra.com

Comment lier rx_tap (UIButton) à ViewModel?

J'ai un contrôleur d'autorisation avec 2 propriétés UITextField et 1 UIButton. Je souhaite lier ma vue à ViewModel mais je ne sais pas comment le faire. C'est mon 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?

}

}

Et voici mon 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
}
}
9
Marina

Vous pouvez transformer les robinets de l'UIButton en observable et le remettre au ViewModel avec les deux observables de UITextFields.

Ceci est un petit exemple de travail pour votre scénario. (J'ai utilisé une petite classe fictive du client autorisé pour simuler la réponse du service):

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

Ce sont les deux parties intéressantes:

// 1: Nous injectons les trois observables dans le ViewModel lorsque nous l'initialisons.

// 2: Ensuite, nous nous abonnons à la sortie du ViewModel pour recevoir le modèle Auth une fois la connexion établie.

Le 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 obtient les 3 observables dans sa méthode init et les connecte à sa sortie:

// 1: Combinez la dernière valeur du champ de texte de connexion et la dernière valeur du champ de mot de passe en un seul observable.

// 2: lorsque l'utilisateur appuie sur le bouton, utilisez la dernière valeur du champ de texte de connexion et la dernière valeur du champ de mot de passe et transmettez-la au service d'autorisation à l'aide de flatMap. Lorsque le client d'authentification renvoie AuthResponse, mappez-le au modèle Auth. Définissez le résultat de cette "chaîne" comme sortie authResponse de la ViewModel

15
joern

Première approche utiliser 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
    })
  }

}

La seconde approche utilise 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
  }
}
6
user3165616

Le problème ici est que vous essayez de faire de votre "viewModel" une classe. Cela devrait être une fonction.

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

Utilisez set it up en faisant ceci dans votre viewDidLoad:

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

Si vous avez plusieurs sorties pour votre modèle de vue, il peut être intéressant de créer une classe (plutôt que de retourner un tuple à partir d'une fonction.) Si vous voulez le faire, alors GithubSignupViewModel1 à partir des exemples du référentiel RxSwift est un excellent exemple de la façon de le configurer.

1
Daniel T.