简体   繁体   English

如何在ViewModel中放置可观察到的RxSwift

[英]How to dispose RxSwift observable in viewmodel

I am learning RxSwift and I have tried a basic login UI using it. 我正在学习RxSwift,并尝试使用它进行基本的登录UI。 My implementation is as follows. 我的实现如下。

LoginViewController: 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: 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. 同样,由于您说过您仍在学习Rx,因此您所拥有的并不是最佳实践。 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. 在您的视图模型中,更喜欢在init函数中接受输入。 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. 如果您不能这样做,例如,如果某些输入尚不存在,则使用Subject属性并将其绑定。 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. DisposeBag 不在视图模型中。

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. 请注意,所有代码最终都在viewDidLoad函数中。 That's the ideal so you don't have to deal with [weak self] . 这是理想的选择,因此您不必面对[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: 在这种情况下,我可能会将onNext闭包放在一个经过咖喱的全局函数中,在这种情况下,它看起来像这样:

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. 请注意,这里的update(fields:)函数不在类中。 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. 您已将一次性用品添加到LoginViewModel对象的处理包中,该对象在释放LoginViewController对象时被释放。
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. 这意味着在释放LoginViewController或您收到areValidFields Observable上的完成或错误之前,将不会处理LoginViewModel observable返回的一次性areValidFields

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: 但是,如果您要在LoginViewController移出屏幕时处置可观察对象,则可以手动处置:

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()
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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