如何在 viewmodel 中配置 RxSwift observable

How to dispose RxSwift observable in viewmodel

我正在学习 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])
        }
    }
}

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

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

您已将 disposable 添加到 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()
}

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

另外,既然你说你还在学习 Rx,那么你所拥有的离最佳实践还很远。我希望看到更多类似这样的内容:

class LoginViewModel {

    let areValidFields: Observable<[Bool]>

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

        let usernameValid = username.map { [=10=].count > 6 }
        let passValid = password.map { [=10=].count > 6 }
        let confirmValid = Observable.combineLatest(password, confirm)
            .map { [=10=] ==  }

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

在您的视图模型中,更喜欢在 init 函数中接受输入。如果你不能这样做,例如如果某些输入尚不存在,则使用 Subject 属性 并绑定到它。但是在任何一种情况下,您的视图模型基本上应该只包含一个初始化函数和一些输出属性。 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:)函数在class中是而不是。这样我们就不会捕获自我,因此不必担心自我脆弱。此外,此更新功能可能对应用中的其他表单输入非常有用。