3
\$\begingroup\$

I am building a simple login page which will check for username before navigating to another screen and here is how I am doing the binding now. I would like to know if I am doing it right and if I am not, what is the recommended way of doing the binding. Moreover, I creating the UI programmatically.

ViewModel.swift

// Inputs
private let usernameSubject = PublishSubject<String>()
private let nextButtonDidTapSubject = PublishSubject<Void>()

// Outputs
private let validUsernameSubject = PublishSubject<Bool>()
private let invalidUsernameSubject = PublishSubject<String>()

// MARK: - Init

init() {

    input = Input(username: usernameSubject.asObserver(),
                  nextButtonDidTap: nextButtonDidTapSubject.asObserver())

    output = Output(validUsername: validUsernameSubject.asObservable(),
                    invalidUsername: invalidUsernameSubject.asObservable())

    nextButtonDidTapSubject
        .withLatestFrom(usernameSubject.asObservable())
        .subscribe(onNext: { [unowned self] text in
            if text.count >= self.minUsernameLength {
                self.validUsernameSubject.onNext(true)
            } else {
                let message = text.count > 0 ?
                    "Please enter a valid username" :
                    "Please enter a username"
                self.invalidUsernameSubject.onNext(message)
            }
        })
        .disposed(by: disposeBag)

}

ViewController.swift

private func configureBinding() {

    loginLandingView.usernameTextField.rx.text.orEmpty
        .subscribe(viewModel.input.username)
        .disposed(by: disposeBag)

    loginLandingView.nextButton.rx.tap
        .subscribe(viewModel.input.nextButtonDidTap)
        .disposed(by: disposeBag)

    viewModel.output.validUsername
        .subscribe(onNext: { [unowned self] _ in
            print("Valid username - Navigate...")
            self.navigate()
        })
        .disposed(by: disposeBag)

    viewModel.output.invalidUsername
        .subscribe(onNext: { [unowned self] message in
            self.showAlert(with: message)
        })
        .disposed(by: disposeBag)

}
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

I do not recommend this approach. There are entirely too many subjects and they are completely unnecessary. Too much boilerplate.

I recommend a more functional approach:

The view controller would contain something like this:

// assign viewModel before presenting.
var viewModel: (LoginInputs) -> LoginOutputs = { _ in fatalError("assign before view is loaded.") }

// called from viewDidLoad()
private func configureBinding() {

    let inputs = LoginInputs(
        username: loginLandingView.usernameTextField.rx.text.orEmpty.asObservable(),
        loginTrigger: loginLandingView.nextButton.rx.tap.asObservable()
    )

    let outputs = viewModel(inputs)

    outputs.navigateTrigger
        .subscribe(onNext: { [unowned self] in
            self.navigate()
        })
        .disposed(by: disposeBag)

    outputs.invalid
        .subscribe(onNext: { [unowned self] message in
            self.showAlert(with: message)
        })
        .disposed(by: disposeBag)
}

And the view model would look like:

struct LoginInputs {
    let username: Observable<String>
    let loginTrigger: Observable<Void>
}

struct LoginOutputs {
    let navigateTrigger: Observable<Void>
    let invalid: Observable<String>
}

func loginViewModel(minUsernameLength: Int) -> (_ inputs: LoginInputs) -> LoginOutputs {
    return { inputs in
        let usernameEntered = inputs.loginTrigger
            .withLatestFrom(inputs.username)

        let navigateTrigger = usernameEntered
            .filter { minUsernameLength <= $0.count }
            .map { _ in }
        let usernameTooShort = usernameEntered
            .filter { 1 <= $0.count && $0.count < minUsernameLength }
            .map { _ in "Please enter a valid username" }
        let usernameEmpty = usernameEntered
            .filter { $0.isEmpty }
            .map { _ in "Please enter a username" }

        return LoginOutputs(
            navigateTrigger: navigateTrigger,
            invalid: Observable.merge(usernameTooShort, usernameEmpty)
        )
    }
}

The code that presents the view controller would look something like:

let controller = LoginViewController() 
controller.viewModel = loginViewModel(minUsernameLength: 8) // or whatever the minimum is.
// show the view controller

The above will maximize the testability of your code. You can test the view model by simply calling the function and pushing data into it. You can test the view controller by assigning a viewModel function that pushes test data.

The above will also establish a strong separation between your logic (in the view model) and your effects (in the view controller.)

\$\endgroup\$
2
  • \$\begingroup\$ Thank you for the recommended approach. Highly appreciate it as I am still learning how to integrate RxSwift with MVVM. As for the last piece of your code regarding the presentation of view controller, wouldn't it be better/cleaner if I am defining the minUsernameLength inside the viewModel since it is unlikely to change OR it is meant for ease of testing \$\endgroup\$ Commented Apr 26, 2019 at 2:41
  • \$\begingroup\$ That's up to you. I defined it outside the view model because it looked like you were doing the same from the code sample you presented. You could put a let minUsernameLength = 8 just before the let usernameEntered line if you would prefer. \$\endgroup\$
    – Daniel T.
    Commented Apr 26, 2019 at 11:25

Not the answer you're looking for? Browse other questions tagged or ask your own question.