【问题标题】:How to dispose RxSwift observable in viewmodel如何在 viewmodel 中处理 RxSwift observable
【发布时间】:2018-08-21 09:27:15
【问题描述】:

我正在学习 RxSwift,我已经尝试了使用它的基本登录 UI。我的实现如下。

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

使用此代码,当我移回导航堆栈时会打印已处理的消息。

但是,如果我继续前进,则不会打印已处理的消息。处置 observable 的正确方法是什么?

【问题讨论】:

    标签: ios dispose rx-swift


    【解决方案1】:

    向前移动时,视图控制器不会从堆栈中移除。它保持不变,以便当用户点击后退按钮时,它已准备好并且仍处于与用户上次看到它时相同的状态。这就是为什么没有处理任何东西。

    另外,既然你说你还在学习 Rx,那么你所拥有的远不是最佳实践。我希望看到更像这样的东西:

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

    在您的视图模型中,更愿意接受init 函数中的输入。如果你不能这样做,例如如果某些输入尚不存在,则使用 Subject 属性并绑定到它。但无论哪种情况,您的视图模型基本上应该只包含一个 init 函数和一些用于输出的属性。 DisposeBag 确实没有进入视图模型。

    您的视图控制器只需要创建一个视图模型并连接到它:

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

    请注意,所有代码都以viewDidLoad 函数结尾。这是理想的选择,因此您不必处理 [weak self]。在这种特殊情况下,我可能会将 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
                }
            }
        }
    }
    

    请注意,update(fields:) 函数在类中不是。这样我们就不会捕获自我,因此不必担心弱自我。此外,此更新功能可能对应用中的其他表单输入非常有用。

    【讨论】:

    • 感谢您的建议和指导。你已经使用了 textFieldUserName.rx.text.orEmpty.asObservable(),我发现我需要使用 RxCocoa。是否建议在使用 RxSwift 时使用 RxCocoa(尽管这似乎不是强制性的)。而且,使用您建议的代码,我无法获得所需的结果。文本字段始终显示红色边框。
    • 验证数组中观察到的值是正确的,ui没有相应更新。也许问题出在 zip 上,我正在研究它。
    • 如果该字段有效,我将更改背景颜色而不是边框​​颜色。我的错。我把它固定在上面。另外,请记住,如果该字段为空,则其计数将小于 7,因此应为红色。
    • 哦,我没注意到。谢谢,现在可以正常使用了。
    • 顺便说一句,严格来说 RxCocoa 不是必需的。但是,它提供的代码确实使 Rx 工作。没有它,你必须自己有效地实现这个库,所以你不妨使用它。
    【解决方案2】:

    您已将一次性物品添加到 LoginViewModel 对象的处置袋中,当 LoginViewController 对象被释放时,该处置袋将被释放。
    这意味着在 LoginViewController 被释放或者您在 areValidFields Observable 上收到完成或错误之前,LoginViewModel observable 返回的一次性对象不会被释放。

    这与大多数可观察到的情况下的公认行为是同步的。

    但是,如果您想在 LoginViewController 移出屏幕时处置 observable,您可以手动处置:

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

    【讨论】:

    • 我们是否应该让系统决定何时发布?还是应该像您回答的那样自己做?哪个是首选方式?
    • @Sujal:这取决于。如果您介意监听可观察事件,当视图不在屏幕上时,您可能希望在离开屏幕后处理。在内存消耗方面,我没有看到任何大的影响。你唯一不想做的就是过度订阅同一个 Observable 而不释放之前的 Observable。
    • 如果手动处理(比如在 viewDidDisappear 中),那么视图控制器在下次出现时将无法工作,因为不会再次调用 viewDidLoad。
    • @DanielT.: 这就是为什么在上面的代码中,我在 viewWillAppear 中订阅了 observable
    猜你喜欢
    • 1970-01-01
    • 2016-06-24
    • 1970-01-01
    • 1970-01-01
    • 2019-10-25
    • 1970-01-01
    • 1970-01-01
    • 2020-12-21
    • 2023-04-01
    相关资源
    最近更新 更多