swiftrx-swiftreactive-cocoa

How can I better combine this 2 values that consume the same Publish Subjects


I am learning RxSwift.

I have setup a view model that responds to bindings in my ViewController.

isValid checks both a username and password exist and then enables my login button.

didTapLoginSubject fires on login press, using the latest value from credentialsObservable will call a service.

This all works as I'd like, however I feel something is not quite optimised around how isValid and credentialsObservable work.

I see repeated code and sense this can be better written, but I am not sure how yet.

I thought perhaps something like this:

    private(set) lazy var isValid: Observable<Bool> = {
        return Observable.withLatestFrom(self.credentialsObservable).map { $0.count > 0 && $1.count > 0 }
    }()

But this obviously did not work.


import Foundation
import RxSwift
import RxCocoa

class LoginViewModel: NSObject {
    private(set) lazy var username = PublishSubject<String>()
    private(set) lazy var password = PublishSubject<String>()
    private(set) lazy var didTapLoginSubject = PublishSubject<Void>()

    private(set) lazy var isValid: Observable<Bool> = {
        return Observable.combineLatest(self.username, self.password, resultSelector: { $0.count > 0 && $1.count > 0 })
    }()

    private var credentialsObservable: Observable<(String, String)> {
        return Observable.combineLatest(self.username, self.password, resultSelector: { ($0, $1) })
    }

    private let disposeBag = DisposeBag()

    override init() {
        super.init()

        didTapLoginSubject
            .withLatestFrom(credentialsObservable)
            .subscribe(
                onNext: login,
                onError: onError
        ).disposed(by: disposeBag)
    }

    private func login(_ username: String, _ password: String) {
        print(username, password)
    }

    private func onError(_ error: Error) {
        print(error.localizedDescription)
    }
}


Solution

  • Your view will get new credentials only when something is changed (because PublishSubjects work this way). So, it might be useful to store your credentials in BehaviorSubject and get the latest state when view subscribes to the view model. It is necessary if you provide some profiled state for username/password.

    private lazy var credentials = BehaviorSubject<(String, String)?>(value: nil)
    

    And prepare the binding in init:

    Observable
        .combineLatest(username, password){ ($0, $1) }
        .bind(to: credentials)
        .disposed(by: disposeBag)
    

    You can also use your stored credentials for isValid check and for didTapLoginSubject:

    var isValid: Observable<Bool> {
        return credentials
            .asObservable()
            .map({ (credentials) -> Bool in
                guard let credentials = credentials else {
                    return false
                }
                return credentials.0.count > 0 && credentials.1.count > 0
            })
            .distinctUntilChanged()
    }
    
    didTapLoginSubject
        .withLatestFrom(credentials)
        .filterNil()
        .subscribe(
            onNext: login,
            onError: onError
        ).disposed(by: disposeBag)