1

I have a LoginViewModel and LoginViewController class. In LoginViewController there is 2 textfield userName, password, and a Login Button, when user click Login button username and password field is validated in LoginViewModel class, if it is empty then a respective message is passed to LoginViewController class and message in displayed in respective textfield.

class LoginViewModel : LoginViewModelProtocol {

    var errorObservable: PublishSubject<String> = PublishSubject<String>()

    var userName: BehaviorRelay<String> = BehaviorRelay(value: "")
    var password: BehaviorRelay<String> = BehaviorRelay(value: "")
    let disposeBag = DisposeBag()
    var apiClient : ApiClientProtocol
    public init(fetcher : ApiClientProtocol) {
        apiClient = fetcher
    }

    func validateUserName(_ value: String) -> Bool {
        if value.count == 0 {
            errorObservable.onNext("This field is required")
            return false
        }
        return true
    }

    func validatePassword(_ value: String) -> Bool {
        if value.count == 0 {
            errorObservable.onNext("This field is required")
            return false
        }
        return true
    }




    func onLoginButtonClick(){
        if self.validateUserName(userName.value) &&  self.validatePassword(password.value) {
//        apiClient.performLogin(userName: userName.value, password: password.value)
        }

    }

}

//validateUserName and validatePassword should notify username and password field respectively.

class LoginViewController: UIViewController {


    @IBOutlet weak var usernameTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var loginButton: UIButton!

    let disposeBag = DisposeBag()

  var viewModel: LoginViewModelProtocol!

   override func viewDidLoad() {
        super.viewDidLoad()
        initialiseUI()
        configureBinding()
  }

  func configureBinding() {
        usernameTextField.rx.text
            .orEmpty
            .bind(to: viewModel.userName)
            .disposed(by: disposeBag)

        passwordTextField.rx.text
            .orEmpty
            .bind(to: viewModel.password)
            .disposed(by: disposeBag)



  viewModel.errorObservable.asObserver().subscribe(onNext: { (error) in

            self.updateUI(error)

        }, onDisposed: {})

    }

    func updateUI(_ error : String){

        let fontS = UIFont.systemFont(ofSize: 12)
        let attributes = [
            NSAttributedString.Key.foregroundColor: UIColor.red,
            NSAttributedString.Key.font : fontS
            ] as [NSAttributedString.Key : Any]


        self.passwordTextField.attributedPlaceholder = NSAttributedString(string: error, attributes: attributes)
    }
}

enter image description here

I want to show error message in both textfield when both are empty How will I pass value with errorObserver for both textField and password when they are empty with RxSwift?

Jasmine John
  • 873
  • 8
  • 12

1 Answers1

1

The key here is to make two different error outputs. Something like the below would work, although I'm not a fan of having so many subjects...

final class LoginViewController: UIViewController {
    @IBOutlet weak var usernameTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var loginButton: UIButton!

    var viewModel: LoginViewModel!

    override func viewDidLoad() {
        super.viewDidLoad()

        let inputs = LoginInputs(
            userName: usernameTextField.rx.text.orEmpty.asObservable(),
            password: passwordTextField.rx.text.orEmpty.asObservable(),
            trigger: loginButton.rx.tap.asObservable()
        )

        viewModel.attach(inputs)

        disposeBag.insert(
            bind(input: viewModel.userNameError, output: usernameTextField.rx.attributedText),
            bind(input: viewModel.passwordError, output: passwordTextField.rx.attributedText),
            viewModel.loginSuccess.bind(onNext: { print("login success:", $0) })
        )
    }

    func bind(input: Observable<String>, output: ControlProperty<NSAttributedString?>) -> Disposable {
        let attributes = [
            NSAttributedString.Key.foregroundColor: UIColor.red,
            NSAttributedString.Key.font : UIFont.systemFont(ofSize: 12)
            ] as [NSAttributedString.Key : Any]

        return input
            .map { NSAttributedString(string: $0, attributes: attributes) }
            .bind(to: output)
    }

    private let disposeBag = DisposeBag()
}

struct LoginInputs {
    let userName: Observable<String>
    let password: Observable<String>
    let trigger: Observable<Void>
}

struct LoginViewModel {
    let userNameError: Observable<String>
    let passwordError: Observable<String>
    let loginSuccess: Observable<Bool>

    init(fetcher: ApiClientProtocol) {
        self.fetcher = fetcher
        userNameError = _userNameError.asObservable()
        passwordError = _passwordError.asObservable()
        loginSuccess = _loginSuccess.asObservable()
    }

    func attach(_ inputs: LoginInputs) {
        disposeBag.insert(
            bind(trigger: inputs.trigger, input: inputs.userName, output: _userNameError.asObserver()),
            bind(trigger: inputs.trigger, input: inputs.password, output: _passwordError.asObserver())
        )

        let credentials = Observable.combineLatest(inputs.userName, inputs.password) { (username: $0, password: $1) }
        inputs.trigger
            .withLatestFrom(credentials)
            .filter { !$0.username.isEmpty && !$0.password.isEmpty}
            .flatMapLatest { [fetcher] in
                fetcher.performLogin(userName: $0.username, password: $0.password)
                    .map { true }
                    .catchErrorJustReturn(false)
            }
            .bind(to: _loginSuccess)
            .disposed(by: disposeBag)
    }

    private func bind(trigger: Observable<Void>, input: Observable<String>, output: AnyObserver<String>) -> Disposable {
        return trigger
            .withLatestFrom(input)
            .filter { $0.isEmpty }
            .map { _ in "This field is required" }
            .bind(to: output)
    }

    private let fetcher: ApiClientProtocol
    private let _userNameError = PublishSubject<String>()
    private let _passwordError = PublishSubject<String>()
    private let _loginSuccess = PublishSubject<Bool>()
    private let disposeBag = DisposeBag()
}

protocol ApiClientProtocol {
    func performLogin(userName: String, password: String) -> Observable<Void>
}
Daniel T.
  • 32,821
  • 6
  • 50
  • 72