需求:输入手机号、密码,并校验手机号、密码格式是否正确,并给出相应的提示,然后点击登录,发起网络请求,登录成功跳转界面;
具体实现效果可以参考下图:
1. View层
主要是视图布局,这里不再罗列所有代码
override init(frame: CGRect) { super.init(frame: frame) addSubview(phoneTextFied) addSubview(pwdTextFied) addSubview(loginButton) addSubview(warnLabel) }
2. Model
这里model
层仅用作演示,这里并未真正用到model
层,实际开发中,可能需要在登录成功后,做一些数据的存储,这里不做演示:
class LoginModel { lazy var isLoginSuccess: Bool = false }
3. ViewModel
Input:
phone
:手机号输入框输入内容事件
pwd
:密码输入框输入内容事件
login
:按钮点击事件
Output:
phoneValid
:当手机号格式不对的时候,给外界绑定到label
的显示提示用;
phoneLimit
:限制手机号只能输入11位,并将结果用于绑定到手机号输入的文本上;
pwdValid
:当密码格式不对的时候,给外界绑定到label
的显示提示用;
loginEnabled
:登录按钮是否可以点击,用于给外界绑定按钮的状态使用;
loginResult
:点击登录按钮后,发起请求,将请求的结果回调给外界,做跳转用;
localPhoneNumber:
用于保存本地的手机号码,这里仅用作演示用,可以忽略;
loading:
网络请求中,这里仅用作演示用,可以忽略;
typealias PhonePwd = (phone: String, pwd: String) struct LoginViewModel { let localPhoneNumber: BehaviorRelay<String?> = BehaviorRelay(value: nil) let loading: BehaviorRelay<Bool> = BehaviorRelay(value: false) struct Input { let phone: ControlProperty<String?> let pwd: ControlProperty<String?> let login: ControlEvent<Void> } struct Output { let phoneValid: Driver<Bool> let phoneLimit: Observable<String> let pwdValid: Driver<Bool> let loginEnabled: Driver<Bool> let loginResult: Observable<Bool> } func transform(input: Input) -> Output { localPhoneNumber.accept("12345678901") //获取默认手机号 let _phone = input.phone.orEmpty.throttle(.milliseconds(100), scheduler: MainScheduler.instance).flatMap { (text) -> Observable<String> in if text.count > 11 { return Observable.just(String(text.prefix(11))) } return Observable.just(text) } let _pwd = input.pwd.orEmpty.throttle(.milliseconds(100), scheduler: MainScheduler.instance) let phoneValid = _phone.flatMap { (text) -> Observable<Bool> in return Observable.just(text.count == 11) }.asDriver(onErrorJustReturn: false) let pwdValid = _pwd.flatMap { (text) -> Observable<Bool> in return Observable.just(text.count > 5) }.asDriver(onErrorJustReturn: false) let loginEnabled = SharedSequence.combineLatest(phoneValid, pwdValid, loading.asDriver()).flatMap { (phone, pwd, isLoading) -> SharedSequence<DriverSharingStrategy, Bool> in return SharedSequence<DriverSharingStrategy, Bool>.just(phone && pwd && !isLoading) } let _result = Observable.combineLatest(_phone, _pwd).flatMap { (phone, pwd) -> Observable<PhonePwd> in return Observable.just((phone, pwd)) } let loginResult = input.login.withLatestFrom(_result).flatMapLatest { self.request($0, $1) }.observe(on: MainScheduler.instance) return Output(phoneValid: phoneValid, phoneLimit: _phone, pwdValid: pwdValid, loginEnabled: loginEnabled, loginResult: loginResult) } private func request(_ phone: String, _ pwd: String) -> Observable<Bool> { Observable<Bool>.create { (observer) -> Disposable in Logger("发起请求 Loading... \(Thread.current)") self.loading.accept(true) DispatchQueue.global().asyncAfter(deadline: .now() + 2) { Logger("收到响应 End Loading... \(Thread.current)") self.loading.accept(false) if phone == "12345678901" && pwd == "123456" { observer.onNext(true) }else { observer.onNext(false) } } return Disposables.create() }.subscribe(on: ConcurrentDispatchQueueScheduler(qos: .userInteractive)) } }
4. Controller
将ViewModel
中处理后的事件绑定到UI
控件上:
private func bindToViewModel() { let output = viewModel.transform(input: LoginViewModel.Input(phone: loginView.phoneTextFied.rx.text, pwd: loginView.pwdTextFied.rx.text, login: loginView.loginButton.rx.tap)) _ = loginView.phoneTextFied.rx.text <-> viewModel.localPhoneNumber//演示双向绑定 output.phoneLimit.bind(to: loginView.phoneTextFied.rx.text).disposed(by: disposeBag)//演示bind output.phoneValid.drive(loginView.warnLabel.phoneWarnBinder).disposed(by: disposeBag)//演示driver output.pwdValid.drive(loginView.warnLabel.pwdWarnBinder).disposed(by: disposeBag)//演示给Label扩展Binder output.loginEnabled.drive(loginView.loginButton.rx.isEnabled).disposed(by: disposeBag)//演示给Rective的Button扩展Binder output.loginEnabled.drive(loginView.loginButton.rx.bindLoginBackground).disposed(by: disposeBag)//演示给Rective的Button扩展Binder viewModel.loading.asDriver().drive(loginView.loginButton.rx.loginLoadingTitle).disposed(by: disposeBag)//演示通过viewModel内的属性直接绑定 output.loginResult.subscribe(onNext: { res in //演示登录/失败做其他操作 if res { Logger("登录成功 - 跳转界面 - \(Thread.current)") }else { Logger("登录失败 - 请重新登录 - \(Thread.current)") } }).disposed(by: disposeBag) }
对Reactive
扩展Binder
,把值绑定到button
上:
extension Reactive where Base == UIButton { fileprivate var bindLoginBackground: Binder<Bool> { Binder<Bool>(self.base) { (item, value) in if value { item.backgroundColor = .blue }else { item.backgroundColor = .gray } } } fileprivate var loginLoadingTitle: Binder<Bool> { Binder<Bool>(self.base) { (item, value) in if value { item.setTitle("正在登录...", for: .normal) }else { item.setTitle("登录", for: .normal) } } } }
对UILabel
扩展Binder
,把值绑定到label
上:
extension UILabel { fileprivate var phoneWarnBinder: Binder<Bool> { Binder(self) { (label, canUse) in if canUse { label.text = "手机号可用" label.textColor = .blue }else { label.text = "手机号格式不正确" label.textColor = .red } } } fileprivate var pwdWarnBinder: Binder<Bool> { Binder(self) { (label, canUse) in if canUse { label.text = "密码可用" label.textColor = .blue }else { label.text = "密码格式不正确" label.textColor = .red } } } }
藏划线