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

时间:2018-08-21 09:27:15

标签: ios dispose rx-swift

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

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

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

2 个答案:

答案 0 :(得分:2)

向前移动时,不会从堆栈中删除视图控制器。它保留下来,以便当用户点击“后退”按钮时,它已经准备好,并且仍处于与用户上次看到它相同的状态。这就是为什么什么也没有处置的原因。

此外,由于您说过您仍在学习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属性并将其绑定。但是无论哪种情况,您的视图模型基本上都应该只包含一个初始化函数和一些用于输出的属性。 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:)函数在该类中不是。这样,我们就不会捕获自我,因此不必担心脆弱的自我。另外,此更新功能可能对应用程序中的其他表单输入很有用。

答案 1 :(得分:0)

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

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

但是,如果要在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()
}