반응식 프로그래밍을 응용한 보기 모델 입출력 방법

리액션 프로그래밍에서 MVVM 아키텍처를 사용하기 시작한 이래로, 나는 줄곧 비슷한 아키텍처를 찾고 있다. 이것은 나의 수요를 충족시키고 더욱 매력적일 것이다.나는 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 }
}

세분화:


보시다시피 저는 세 가지 협의가 있습니다.

  • 입력 - 주로 보기 컨트롤러나 필요한 조작에서 나온다.
    보시다시피 isEmailValid와 isPasswordValid는 부울 값이 아니지만, 유효성을 표시하기 위해 매거진을 만들었습니다.왜?이따가 보게 될 거야.

  • 내보내기 - 뷰 모델 외부에 표시되는 값입니다.

  • 유형 - 입력과 출력의 포장기.이것은 경로 감각을 가져오고 보기 모델에서 접근성을 제어하는 데 도움이 되며, 잠시 후에 우리가 왜 그것을 필요로 하는지 보실 수 있습니다.
  • 다음은 뷰 모델 구현입니다.

    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()에서 그것을 직접 검증하는 것이 아니라 SigninViewModel에서 그것을 관찰할 수 있습니다.
    이제 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)
    
  • 이메일과 비밀번호의 입력만 확인하고 유효한지 확인한 다음 isEmailValid와 isPasswordValid
  • 에 연결합니다

    잘못된 경우 오류 메시지 반환
    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와 isPasswordValid가 터치되었습니다. 현재 각각의 값이 있습니다. 만약 그것이 무효라면 오류 메시지를 되돌려 주고 싶습니다.

  • 오류 메시지가 유효하면 비어 있음
    isEmailValid.filter { $0 == .valid }
        .map { _ in "" }
        .bind(to: emailNotValidErrMssg)
        .disposed(by: disposeBag)
    
    isPasswordValid.filter { $0 == .valid }
        .map { _ in "" }
        .bind(to: passwordNotValidErrMssg)
        .disposed(by: disposeBag)
    
  • 현재 오류 메시지가 유효하면 삭제합니다.
  • 이제 보기 컨트롤러에 적용해 보겠습니다.

    컨트롤러에 로그인합니다.날래다


    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)
        }
    }
    
    이제 분석해 봅시다.
  • 먼저, 저는 보기 모델과 이 보기 컨트롤러의 하위 보기를 초기화했습니다. DisposeBag을 포함합니다.
  • 만약에 내가 viewModel을 변수 SigninViewModelTypes의 데이터 형식으로 하지 않고 SigninViewModel을 사용한 것을 알아차린다면 왜?만약에 내가 inputs을 사용한다면 나는 클래스의 변수를 직접 방문할 수 있다. 이것은 내가 사용하고 싶은 outputsviewModel.inputs.someFunction() 프로토콜을 돌아갈 것이다. 그래서 나는 의외로 viewModel.someFunction()을 사용할 수 있다. 이것은 내가 피하고 싶은 것이다.
  • 하위 보기 설정을 건너뛰고 setupBindings()의 귀속에 중점을 두겠습니다. 이제 분해하겠습니다.

    viewModel 입력 함수에 from textField 바인딩
    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)
    

    입력한 전자 메일 또는 암호의 유효성에 따라 textField 테두리 색상 변경
    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을 사용했는지 기억나?이것이 바로 내가 borderColor를textField의 유효성 상태에 추가하고 싶은 이유이다.나는 이렇게 했다.
  • enum TextFieldStatus {
        case valid, notValid
    
        var borderColor: CGColor {
            switch self {
            case .valid: return UIColor.lightGray.cgColor
            default: return UIColor.red.cgColor
            }
        }
    }
    
  • 저는 borderColor이라는 변수를 추가했고 사례에 따라 cgColor를 정의했습니다.이것이 바로 우리가 isPasswordValid를 cgColor에 비추어 예로 삼아 텍스트 필드의borderColor에 연결할 수 있는 이유입니다. 그러나 제가 어떻게 했는지 알고 싶다면 borderColor은 RxSwift에서 Binder으로 제공할 수 없다는 것을 알고 있습니다.확장자를 만들었습니다. 다음은 코드입니다.
  • extension Reactive where Base: UITextField {
        public var borderColor: Binder<CGColor> {
            return Binder(base, binding: { textField, active in
                textField.layer.borderColor = active
            })
        }
    }
    
  • 현재 나는 엔움의borderColor를textField의borderColor에 직접 연결할 수 있다.
  • 다음에 입력한 데이터가 올바르지 않으면viewModel의 오류를 표시하고 싶습니다. 다음에 어떻게 해야 합니까?
    viewModel.outputs.emailNotValidErrMssg.bind(to: emailErrLabel.rx.text).disposed(by: disposeBag)
    viewModel.outputs.passwordNotValidErrMssg.bind(to: passwordErrLabel.rx.text).disposed(by: disposeBag)
    
    이제 텍스트 필드에 대한 검증이 완료되었습니다. 다음은 텍스트 필드의 모양입니다.

    이런 구조는 내가 돌연변이와 접근 가능한 변수를 분리하는 데 도움을 준다.두 번째 부분은 제작 중입니다. 이런 방법으로 단원 테스트를 하는 것이 얼마나 쉬운지 토론하겠습니다.참고로 이것은 이 프로젝트의 repository입니다.

    좋은 웹페이지 즐겨찾기