简体   繁体   中英

How to dispose RxSwift observable in viewmodel

I am learning RxSwift and I have tried a basic login UI using it. My implementation is as follows.

LoginViewController:

fileprivate let loginViewModel = LoginViewModel()

fileprivate var textFieldArray: [UITextField]!

override func viewDidLoad() {
    super.viewDidLoad()

    textFieldArray = [textFieldUserName, textFieldPassword, textFieldConfirmPassword]

    textFieldUserName.delegate = self
    textFieldPassword.delegate = self
    textFieldConfirmPassword.delegate = self

    loginViewModel.areValidFields.subscribe(
        onNext: { [weak self] validArray in
            for i in 0..<validArray.count {
                if validArray[i] {
                    self?.showValidUI(index: i)
                } else {
                    self?.showInValidUI(index: i)
                }
            }
        },
        onCompleted: {
            print("### COMPLETED ###")
        },
        onDisposed: {
            print("### DISPOSED ###")
    }).disposed(by: loginViewModel.bag)

}

func showValidUI(index: Int) {
    textFieldArray[index].layer.borderColor = UIColor.clear.cgColor
}

func showInValidUI(index: Int) {
    textFieldArray[index].layer.borderColor = UIColor.red.cgColor
    textFieldArray[index].layer.borderWidth = 2.0
}

extension LoginViewController: UITextFieldDelegate {

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

        let inputText = (textField.text! as NSString).replacingCharacters(in: range, with: string)

        switch textField {
        case textFieldUserName:
            loginViewModel.updateUserName(text: inputText)
        case textFieldPassword:
            loginViewModel.updatePassword(text: inputText)
        case textFieldConfirmPassword:
            loginViewModel.updateConfirmedPassword(text: inputText)
        default:
            return false
        }
        return true
    }  
}

LoginViewModel:

class LoginViewModel {

    private var username: String!
    private var password: String!
    private var confirmedPassword: String!

    fileprivate let combinedSubject = PublishSubject<[Bool]>()

    let bag = DisposeBag()


    var areValidFields: Observable<[Bool]> {
        return combinedSubject.asObservable()
    }

    init() {
        self.username = ""
        self.password = ""
        self.confirmedPassword = ""
    }

    /*deinit {
        combinedSubject.onCompleted()
    }*/


    func updateUserName(text: String) {
        username = text
        if username.count > 6 {
            combinedSubject.onNext([true, true, true])
        } else {
           combinedSubject.onNext([false, true, true])
        }
    }

    func updatePassword(text: String) {
        password = text
        if password.count > 6 {
            combinedSubject.onNext([true, true, true])
        } else {
            combinedSubject.onNext([true, false, true])
        }
    }

    func updateConfirmedPassword(text: String) {
        confirmedPassword = text
        if confirmedPassword == password {
            combinedSubject.onNext([true, true, true])
        } else {
            combinedSubject.onNext([true, true, false])
        }
    }
}

With this code, the disposed message gets printed when i move back the navigation stack.

However, if I move forward, the disposed message is not printed. What is the proper way to dispose the observable?

When you move forward, the view controller is not removed from the stack. It remains so that when the user taps the back button, it is ready and still in the same state as the last time the user saw it. That is why nothing is disposed.

Also, since you said you are still learning Rx, what you have is not anywhere near best practices. I would expect to see something more like this:

class LoginViewModel {

    let areValidFields: Observable<[Bool]>

    init(username: Observable<String>, password: Observable<String>, confirm: Observable<String>) {

        let usernameValid = username.map { $0.count > 6 }
        let passValid = password.map { $0.count > 6 }
        let confirmValid = Observable.combineLatest(password, confirm)
            .map { $0 == $1 }

        areValidFields = Observable.combineLatest([usernameValid, passValid, confirmValid])
    }
}

In your view model, prefer to accept inputs in the init function. If you can't do that, for eg if some of the inputs don't exist yet, then use a Subject property and bind to it. But in either case, your view model should basically consist only of an init function and some properties for output. The DisposeBag does not go in the view model.

Your view controller only needs to create a view model and connect to it:

class LoginViewController: UIViewController {

    @IBOutlet weak var textFieldUserName: UITextField!
    @IBOutlet weak var textFieldPassword: UITextField!
    @IBOutlet weak var textFieldConfirmPassword: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()

        let viewModel = LoginViewModel(
            username: textFieldUserName.rx.text.orEmpty.asObservable(),
            password: textFieldPassword.rx.text.orEmpty.asObservable(),
            confirm: textFieldConfirmPassword.rx.text.orEmpty.asObservable()
        )

        let textFieldArray = [textFieldUserName!, textFieldPassword!, textFieldConfirmPassword!]

        viewModel.areValidFields.subscribe(
            onNext: { validArray in
                for (field, valid) in zip(textFieldArray, validArray) {
                    if valid {
                        field.layer.borderColor = UIColor.clear.cgColor
                    }
                    else {
                        field.layer.borderColor = UIColor.red.cgColor
                        field.layer.borderWidth = 2.0
                    }
                }
            })
            .disposed(by: bag)

    }

    private let bag = DisposeBag()
}

Notice that all of the code ends up in the viewDidLoad function. That's the ideal so you don't have to deal with [weak self] . In this particular case, I would likely put the onNext closure in a curried global function, in which case it would look like this:

class LoginViewController: UIViewController {

    @IBOutlet weak var textFieldUserName: UITextField!
    @IBOutlet weak var textFieldPassword: UITextField!
    @IBOutlet weak var textFieldConfirmPassword: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()

        let viewModel = LoginViewModel(
            username: textFieldUserName.rx.text.orEmpty.asObservable(),
            password: textFieldPassword.rx.text.orEmpty.asObservable(),
            confirm: textFieldConfirmPassword.rx.text.orEmpty.asObservable()
        )

        let textFieldArray = [textFieldUserName!, textFieldPassword!, textFieldConfirmPassword!]

        viewModel.areValidFields.subscribe(
            onNext:update(fields: textFieldArray))
            .disposed(by: bag)

    }

    private let bag = DisposeBag()
}

func update(fields: [UITextField]) -> ([Bool]) -> Void {
    return { validArray in
        for (field, valid) in zip(fields, validArray) {
            if valid {
                field.layer.borderColor = UIColor.clear.cgColor
            }
            else {
                field.layer.borderColor = UIColor.red.cgColor
                field.layer.borderWidth = 2.0
            }
        }
    }
}

Notice here that the update(fields:) function is not in the class. That way we aren't capturing self and so don't have to worry about weak self. Also, this update function may very well be useful for other form inputs in the app.

You have added disposable in to the dispose bag of LoginViewModel object, which gets released when LoginViewController object gets released.
This means the disposable returned by LoginViewModel observable won't be disposed until LoginViewController gets released or you receive completed or error on areValidFields Observable.

This is in sync with the accepted behaviour in most of the observable cases.

But, in case if you want to dispose the observable when LoginViewController moves out of screen, you can manually dispose:

var areValidFieldsDisposbale:Disposable?

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    areValidFieldsDisposbale = loginViewModel.areValidFields.subscribe(
        onNext: { [weak self] validArray in
            for i in 0..<validArray.count {
                if validArray[i] {
                    self?.showValidUI(index: i)
                } else {
                    self?.showInValidUI(index: i)
                }
            }
        },
        onCompleted: {
            print("### COMPLETED ###")
    },
        onDisposed: {
            print("### DISPOSED ###")
    })
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    areValidFieldsDisposbale?.dispose()
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM