How to implement firebase auth into MVVM-C RxSwift

Viewed 787

I'm trying to implement a MVVM-C rx swift app.

I have a VM for my sign up view controller with the username and password as behaviour subjects. I also have a firebase handler injected into the VM. What is the best way to pass the sign up result back to the VC?

my VM code:

class CreateVM {

    let firebase: FirebaseHandler
    let email: String

    var password = BehaviorSubject<String>(value: "")
    var confirmPassword = BehaviorSubject<String>(value: "")

    var shouldHideButton: Observable<Bool> {
        return Observable.combineLatest(password.asObservable(), confirmPassword.asObservable()) { pass, confPass in
            !(pass.count >= 5 && pass == confPass)
        }
    }

    init(firebase: FirebaseHandler, email: String) {
        self.firebase = firebase
        self.email = email
    }

    func submit() {
        let pass = try! password.value()
        firebase.createWithEmail(email: email, password: pass) { (result) in
            switch result {
            case .success(let uid):
                print(uid, "created")
                //handle successful creation
            case .failure(let err):
                print("failed with error:", err)
                //handler error
            }
        }
    }
}

My VC code:

class CreateVC: UIViewController, Storyboarded {

    @IBOutlet weak var createButton: Rounded!
    @IBOutlet weak var passwordEntry: UITextField!
    @IBOutlet weak var confirmPasswordEntry: UITextField!

    weak var coordinator: AuthCoordinator?

    var displayName: String!
    var viewModel: CreateVM!
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        bindUI()
    }

    func bindUI() {
        passwordEntry.rx.text.orEmpty.bind(to: viewModel.password).disposed(by: disposeBag)
        confirmPasswordEntry.rx.text.orEmpty.bind(to: viewModel.confirmPassword).disposed(by: disposeBag)
        viewModel.shouldHideButton.bind(to: createButton.rx.isHidden).disposed(by: disposeBag)

        createButton.rx.tap.bind { [unowned self] _ in
                self.viewModel.submit()
        }.disposed(by: disposeBag)

    }
}
1 Answers

I do my view models as a function, and a lot depends on exactly what you want to do with the result, but here is some sample code that might help you:

struct CreateInput {
    let password: Observable<String>
    let confirm: Observable<String>
    let submit: Observable<Void>
}

struct CreateOutput {
    let displayName: String
    let shouldHideButton: Observable<Bool>
    let signUpResult: Observable<Result<Int, Error>>
}

func createVM(firebase: FirebaseHandler, email: String) -> (CreateInput) -> CreateOutput {
    return { input in
        let shouldHideButton = Observable.combineLatest(input.password, input.confirm) { $0.count < 5 || $0 != $1 }
        let credentials = Observable.combineLatest(Observable.just(email), input.password) { (email: $0, password: $1) }
        let signUpResult = input.submit
            .withLatestFrom(credentials)
            .flatMapLatest {
                firebase.create(email: $0.email, password: $0.password)
        }

        return CreateOutput(
            displayName: email,
            shouldHideButton: shouldHideButton,
            signUpResult: signUpResult
        )
    }
}

extension FirebaseHandler {
    func create(email: String, password: String) -> Observable<Result<Int, Error>> {
        Observable.create { observer in
            self.createWithEmail(email: email, password: password) { (result) in
                observer.onNext(result)
                observer.onCompleted()
            }
            return Disposables.create()
        }
    }
}

final class CreateViewController: UIViewController {

    @IBOutlet weak var displayNameLabel: UILabel!
    @IBOutlet weak var createButton: UIButton!
    @IBOutlet weak var passwordEntry: UITextField!
    @IBOutlet weak var confirmPasswordEntry: UITextField!

    var bindUI: (CreateInput) -> CreateOutput = { _ in fatalError() } // assign `createVM(firebase: myFirebaseHandler, email: "myEmail")` to this before it loads.
    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        let input = CreateInput(
            password: passwordEntry.rx.text.orEmpty.asObservable(),
            confirm: confirmPasswordEntry.rx.text.orEmpty.asObservable(),
            submit: createButton.rx.tap.asObservable()
        )
        let output = bindUI(input)

        displayNameLabel.text = output.displayName

        output.shouldHideButton
            .bind(to: createButton.rx.isHidden)
            .disposed(by: disposeBag)

        output.signUpResult
            .bind { result in
                switch result {
                case .success(let uid):
                    print("uid:", uid)
                case .failure(let error):
                    print("error:", error.localizedDescription)
                }
            }
            .disposed(by: disposeBag)
    }
}

If higher order functions make you nervous, then you could wrap it in a type:

struct CreateVM {

    struct Input {
        let password: Observable<String>
        let confirm: Observable<String>
        let submit: Observable<Void>
    }

    struct Output {
        let displayName: String
        let shouldHideButton: Observable<Bool>
        let signUpResult: Observable<Result<Int, Error>>
    }

    let firebase: FirebaseHandler
    let email: String

    func bind(_ input: Input) -> Output {
        let shouldHideButton = Observable.combineLatest(input.password, input.confirm) { $0.count < 5 || $0 != $1 }
        let credentials = Observable.combineLatest(Observable.just(email), input.password) { (email: $0, password: $1) }
        let signUpResult = input.submit
            .withLatestFrom(credentials)
            .flatMapLatest { [unowned firebase] in
                firebase.create(email: $0.email, password: $0.password)
            }

        return Output(
            displayName: email,
            shouldHideButton: shouldHideButton,
            signUpResult: signUpResult
        )
    }
}

Then your view controller would have a property: var viewModel: CreateVM! and build the output with: let output = viewModel.bind(input)

Related