RxSwift+MVVM项目实战-登录功能实现

简介: RxSwift+MVVM项目实战-登录功能实现

需求:输入手机号、密码,并校验手机号、密码格式是否正确,并给出相应的提示,然后点击登录,发起网络请求,登录成功跳转界面;

具体实现效果可以参考下图:

image.png


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
            }
        }
    }
}

划线


相关文章
|
存储 移动开发 开发框架
【微信小程序 | 实战开发】常用小程序框架介绍
【微信小程序 | 实战开发】常用小程序框架介绍
3696 0
【微信小程序 | 实战开发】常用小程序框架介绍
|
3月前
|
小程序
【微信小程序】-- 自定义组件 - 数据监听器 - 案例 (三十五)
【微信小程序】-- 自定义组件 - 数据监听器 - 案例 (三十五)
|
11月前
|
存储 前端开发
RxSwift+MVVM项目实战-MVVM架构介绍以及实战初体验
RxSwift+MVVM项目实战-MVVM架构介绍以及实战初体验
343 0
|
小程序
微信小程序开发入门与实战(组件生命周期)
微信小程序开发入门与实战(组件生命周期)
微信小程序开发入门与实战(组件生命周期)
|
JSON 小程序 JavaScript
微信小程序开发入门与实战(组件的使用)
微信小程序开发入门与实战(组件的使用)
微信小程序开发入门与实战(组件的使用)
|
设计模式 开发框架 前端开发
手把手教你封装一个健壮的MVP框架,面向接口开发。
在我们的日常开发中,我们都知道 Android 端的开发框架有 MVC,MVP,MVVM,说起这几个框架,大家也肯定都有自己的看法,甚至很多同学也都封装过。
84 0
|
小程序 前端开发 定位技术
【微信小程序 | 实战开发】常用的视图容器类组件介绍和使用(1)
【微信小程序 | 实战开发】常用的视图容器类组件介绍和使用(1)
352 0
【微信小程序 | 实战开发】常用的视图容器类组件介绍和使用(1)
|
小程序 JavaScript 前端开发
【零基础微信小程序入门开发四】小程序框架二
框架的视图层由 WXML 与 WXSS 编写,由组件来进行展示。 将逻辑层的数据反映成视图,同时将视图层的事件发送给逻辑层。 WXML用于描述页面的结构。WXS是小程序的一套脚本语言,结合 WXML,可以构建出页面的结构。WXSS 用于描述页面的样式。 说了那么多,我来概括下: WXML 相当于HTML WXS 相当于JavaS......
156 0
【零基础微信小程序入门开发四】小程序框架二
|
开发框架 小程序 前端开发
【零基础微信小程序入门开发三】小程序框架一
【零基础微信小程序入门开发】小程序介绍及环境搭建 【零基础微信小程序入门开发】配置小程序 👉【零基础微信小程序入门开发】小程序框架 上几节我们学到了小程序的一些基本功能,以及小程序的工具环境配置,大家学习可以顺着系列文章目录来进行查看,如果你有一定基础可以自己选择跳过章节,本节我们在上节的基础上继续讲解小程序的框架小程序开发框架的逻辑层使用 JavaScript 引擎为小程序提供开发者 JavaScript 代码的运行环境以及微信小程序的特有功能。 逻辑层将数据进行处理后发送给视图层,同时接受视图层的事件.
181 0
【零基础微信小程序入门开发三】小程序框架一
|
前端开发
前端项目实战84-基础弹框组件
前端项目实战84-基础弹框组件
85 0

相关实验场景

更多