リアクティブプログラミングを用いたビューモデルのI/Oアプローチ


私が反応プログラミングでMVVMアーキテクチャを使用し始めた時から、私は私のニーズとより魅力的に合う類似した建築を捜していました.私は1つを見つけました、そして、それが完全にそれを適応しなかったとしても、それはKickstarterからあります.
以下は私が今使っているもののサンプルです.私は、ユーザーからの入力データの検証に焦点を当てフォームアプリケーションで簡単な記号を作成しました.
ところで、私はRXSWIFTを反応部分とSnapKitのために使用しています.始めましょう!
// Enum for validity check
enum TextFieldStatus {
    case valid, notValid
}
import RxCocoa
import RxSwift

protocol SigninViewModelInputs {
    func didChange(email: String)
    func didChange(password: String)
}

protocol SigninViewModelOutputs {
    var isEmailValid: PublishRelay<TextFieldStatus> { get }
    var isPasswordValid: PublishRelay<TextFieldStatus> { get }
    var emailNotValidErr: PublishRelay<String> { get }
    var passwordNotValidErr: PublishRelay<String> { get }
}

protocol SigninViewModelTypes {
    var inputs: SigninViewModelInputs { get }
    var outputs: SigninViewModelOutputs { get }
}

ブレイクダウン


ご覧のように、3つのプロトコルがあります.

  • 入力-主にビューコントローラからの操作またはどこにこれを必要とします.
    ご覧のように、IsEmailValidとIssuasswordValidはブール値ではなく、その妥当性を識別するためにenumを作成しました.なぜですか.後で表示されます.

  • 出力-ビューモデルの外側に露出している値.

  • type -入力と出力のラッパー.これはパスの感覚をもたらし、ビューモデルからアクセシビリティを制御するのに役立つ.
  • 次に、ビューモデル実装

    signinviewmodel。スウィフト


    class SigninViewModel: SigninViewModelTypes, SigninViewModelOutputs, SigninViewModelInputs {
        var inputs: SigninViewModelInputs { return self }
        var outputs: SigninViewModelOutputs { return self }
    
        var isEmailValid: PublishRelay<TextFieldStatus> = PublishRelay()
        var isPasswordValid: PublishRelay<TextFieldStatus> = PublishRelay()
        var emailNotValidErr: PublishRelay<String> = PublishRelay()
        var passwordNotValidErr: PublishRelay<String> = PublishRelay()
    
        private var disposeBag: DisposeBag = DisposeBag()
    
        private var didChangeEmailProperty = PublishSubject<String>()
        func didChange(email: String) {
            didChangeEmailProperty.onNext(email)
        }
    
        private var didChangePasswordProperty = PublishSubject<String>()
        func didChange(password: String) {
            didChangePasswordProperty.onNext(password)
        }
    
        init() {
            didChangeEmailProperty.map(isValidEmail(_:)).bind(to: isEmailValid).disposed(by: disposeBag)
    
            isEmailValid.filter { $0 == .notValid }
                .map { _ in "Entered email is not valid." }
                .bind(to: emailNotValidErr)
                .disposed(by: disposeBag)
    
            didChangePasswordProperty
                .map { $0.count > 5 && $0.count < 21 ? .valid : .notValid }
                .bind(to: isPasswordValid)
                .disposed(by: disposeBag)
    
            isPasswordValid.filter { $0 == .notValid }
                .map { _ in "Password has to be from 6 to 20 characters long." }
                .bind(to: passwordNotValidErr)
                .disposed(by: disposeBag)
    
            isEmailValid.filter { $0 == .valid }
                .map { _ in "" }
                .bind(to: emailNotValidErr)
                .disposed(by: disposeBag)
    
            isPasswordValid.filter { $0 == .valid }
                .map { _ in "" }
                .bind(to: passwordNotValidErr)
                .disposed(by: disposeBag)
        }
    
        private func isValidEmail(_ email: String) -> TextFieldStatus {
            let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
    
            let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx)
            return emailPred.evaluate(with: email) ? .valid : .notValid
        }
    }
    

    ブレイクダウン


    private var didChangeEmailProperty = PublishSubject<String>()
    func didChange(email: String) {
        didChangeEmailProperty.onNext(email)
    }
    
    private var didChangePasswordProperty = PublishSubject<String>()
    func didChange(password: String) {
        didChangePasswordProperty.onNext(password)
    }
    
    ご覧のように、入力関数あたりの内部プロパティを作成したので、init()で直接検証することはできません.
    ではinit ()の結合を破壊しましょう

    チェック入力有効性
    didChangeEmailProperty.map(isValidEmail(_:)).bind(to: isEmailValid).disposed(by: disposeBag)
    didChangePasswordProperty
        .map { $0.count > 5 && $0.count < 21 ? .valid : .notValid }
        .bind(to: isPasswordValid)
        .disposed(by: disposeBag)
    
  • これはメールとパスワードの入力をチェックし、有効であればチェックし、isemailとisspwordwordable
  • にバインドします

    有効でない場合にエラーメッセージを返す
    isEmailValid.filter { $0 == .notValid }
       .map { _ in "Entered email is not valid." }
       .bind(to: emailNotValidErr)
       .disposed(by: disposeBag)
    isPasswordValid.filter { $0 == .notValid }
        .map { _ in "Password has to be from 6 to 20 characters long." }
        .bind(to: passwordNotValidErrMssg)
        .disposed(by: disposeBag)
    
  • 現在、IsEmailValidとIssuasswordValidがトリガされているので、それぞれの値が値を持ち、有効でない場合はエラーメッセージを返します.

  • 空のエラーメッセージが有効な場合
    isEmailValid.filter { $0 == .valid }
        .map { _ in "" }
        .bind(to: emailNotValidErrMssg)
        .disposed(by: disposeBag)
    
    isPasswordValid.filter { $0 == .valid }
        .map { _ in "" }
        .bind(to: passwordNotValidErrMssg)
        .disposed(by: disposeBag)
    
  • 現在有効なエラーメッセージを空にします.
  • それでは、ビューコントローラに適用しましょう.

    signinviewcontroller。スウィフト


    class SigninViewController: UIViewController {
    
        var viewModel: SigninViewModelTypes
    
        lazy var emailTextField: UITextField = UITextField()
        lazy var emailErrLabel: UILabel = UILabel()
        lazy var passwordTextField: UITextField = UITextField()
        lazy var passwordErrLabel: UILabel = UILabel()
        lazy var signinButton: UIButton = UIButton()
        lazy var disposeBag = DisposeBag()
    
        init(viewModel: SigninViewModelTypes) {
            self.viewModel = viewModel
            super.init(nibName: nil, bundle: nil)
        }
    
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        override func loadView() {
            super.loadView()
            view.backgroundColor = .white
            setupScene()
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
            setupBindings()
        }
    
        private func setupBindings() {
            emailTextField.rx.text.orEmpty.distinctUntilChanged()
                .bind(onNext: viewModel.inputs.didChange(email:))
                .disposed(by: disposeBag)
    
            passwordTextField.rx.text.orEmpty.distinctUntilChanged()
                .bind(onNext: viewModel.inputs.didChange(password:))
                .disposed(by: disposeBag)
    
            viewModel.outputs.isEmailValid.map { $0.borderColor }
                .bind(to: self.emailTextField.rx.borderColor)
                .disposed(by: disposeBag)
    
            viewModel.outputs.isPasswordValid.map { $0.borderColor }
                .bind(to: self.passwordTextField.rx.borderColor)
                .disposed(by: disposeBag)
    
            viewModel.outputs.emailNotValidErrMssg.bind(to: emailErrLabel.rx.text).disposed(by: disposeBag)
            viewModel.outputs.passwordNotValidErrMssg.bind(to: passwordErrLabel.rx.text).disposed(by: disposeBag)
    
            viewModel.outputs.emailNotValidErrMssg
                .map { $0.isEmpty }
                .bind(to: emailErrLabel.rx.isHidden)
                .disposed(by: disposeBag)
            viewModel.outputs.passwordNotValidErrMssg
                .map { $0.isEmpty }
                .bind(to: passwordErrLabel.rx.isHidden)
                .disposed(by: disposeBag)
        }
    }
    
    今、それを壊しましょう.
  • 最初に、このビューコントローラのビューモデルとサブビューを初期化しました.
  • 現在、私がSigninViewModelを私の変数viewModelのデータ型として入れなかったことに気づいたなら、代わりに、私はSigninViewModelTypesを使用しました.私がSigninViewModelを使用したならば、私は私が使用したいinputsoutputs議定書を迂回するクラスの中で直接変数にアクセスすることができます.
  • サブビューの設定をスキップし、viewModel.inputs.someFunction()の内部にある結合にフォーカスしましょう.

    テキストフィールドからViewModel入力関数への結合
    emailTextField.rx.text.orEmpty.distinctUntilChanged()
        .bind(onNext: viewModel.inputs.didChange(email:))
        .disposed(by: disposeBag)
    
    passwordTextField.rx.text.orEmpty.distinctUntilChanged()
        .bind(onNext: viewModel.inputs.didChange(password:))
        .disposed(by: disposeBag)
    

    入力された電子メールまたはパスワードの妥当性に基づくテキストフィールドの境界線の変化
    viewModel.outputs.isEmailValid.map { $0.borderColor }
       .bind(to: emailTextField.rx.borderColor)
       .disposed(by: disposeBag)
    
    viewModel.outputs.isPasswordValid.map { $0.borderColor }
       .bind(to: passwordTextField.rx.borderColor)
       .disposed(by: disposeBag)
    
  • 私がBoolを使わなかった理由を覚えて、代わりにenumを使いました?このため、ボーダーカラーをTextFieldの妥当性の状態に付けたかったのです.どうやってやったのか
  • enum TextFieldStatus {
        case valid, notValid
    
        var borderColor: CGColor {
            switch self {
            case .valid: return UIColor.lightGray.cgColor
            default: return UIColor.red.cgColor
            }
        }
    }
    
  • 私はviewModel.someFunction()という変数を追加し、ケースに基づいてCGColorを定義しました.そういうわけで、我々は例としてIssasswordValidをCGColorに写像することができて、それをtextfieldの境界色に縛ることができます、しかし、あなたが私がそれを知っているかどうか疑問に思っているならば、待ってください、そして、setupBindings()がRXSwiftでborderColorとして利用できないということを知っていてください.よく、私は拡張をつくりました、そして、それのためのコードはここにあります.
  • extension Reactive where Base: UITextField {
        public var borderColor: Binder<CGColor> {
            return Binder(base, binding: { textField, active in
                textField.layer.borderColor = active
            })
        }
    }
    
  • 現在、境界線をenumからtextfieldの境界色に直接バインドすることができます.
  • 次に、入力データが有効でない場合、ViewModelからエラーを表示します.
    viewModel.outputs.emailNotValidErrMssg.bind(to: emailErrLabel.rx.text).disposed(by: disposeBag)
    viewModel.outputs.passwordNotValidErrMssg.bind(to: passwordErrLabel.rx.text).disposed(by: disposeBag)
    
    では、テキストフィールドの検証を行います.

    この種のアーキテクチャは突然変異とアクセス可能な変数の分離に役立った.パート2は、この種のアプローチでユニットテストを行うのがいかに楽になるかについてです.ところで、このプロジェクトはrepositoryです.