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

划线


相关文章
|
6月前
|
存储 并行计算 算法
CUDA性能优化实战:7个步骤让并行归约算法提升10倍效率
https://avoid.overfit.cn/post/af59d0a6ce474b8fa7a8eafb2117a404
383 1
CUDA性能优化实战:7个步骤让并行归约算法提升10倍效率
|
人工智能 自然语言处理 安全
新浪微博AIGC业务应用探索-AIGC应用平台助力业务提效实践
本次分享围绕AIGC技术在新浪微博的应用展开,涵盖四个部分。首先分析AIGC为微博带来的机遇与挑战,特别是在内容安全和模型幻觉等问题上的应对策略;其次介绍通过工程架构快速实现AIGC技术落地的方法,包括统一部署模型和服务编排;接着展示AIGC在微博的具体应用场景,如评论互动、视频总结和智能客服等;最后展望未来,探讨大模型的发展趋势及其在多模态和特定业务场景中的应用前景。
|
机器学习/深度学习 运维 Python
python深度学习实现自编码器Autoencoder神经网络异常检测心电图ECG时间序列
python深度学习实现自编码器Autoencoder神经网络异常检测心电图ECG时间序列
|
7月前
|
机器学习/深度学习 人工智能 大数据
35岁+大数据人必看:这6个证书,帮你把年龄变成职场「护城河」
35岁不是"职场黄昏",而是"经验红利期"。这些证书不是用来"装门面"的,而是帮你把十几年积累的行业认知,和最新的技术趋势结合起来的"加速器"。 考证的过程,本质上是逼自己跳出舒适区——你可能会重新学Python、研究机器学习模型、梳理数据治理流程,但这些"额外"的努力,都会变成你简历上的亮点、面试时的底气、谈薪资时的筹码。 记住:企业永远愿意为"能解决问题的人"买单。35岁的你,有经验、懂业务、还能学习新东西,这就是你最硬核的竞争力。 现在就开始挑一个证书,把焦虑变成行动力——毕竟,中年危机不可怕,可怕的是你还没开始准备。
|
12月前
|
安全 算法 数据安全/隐私保护
安全漏洞、eBPF、机密计算、商用密码等技术分享|龙蜥大讲堂113期
欢迎大家参加首届中国研究生操作系统创新大赛。我是龙蜥社区安全委员会委员、阿里云技术专家张诗乐。今天,我将为大家介绍龙蜥社区的漏洞治理以及相关的赛题。主要内容分为三个部分:首先,我将简要介绍龙蜥社区及其操作系统;其次,我们将探讨龙蜥社区的漏洞治理流程;最后,我们将解析与龙蜥社区相关的赛题。 1. 龙蜥社区漏洞治理 2. 龙蜥社区赛题介绍 3. 基于 eBPF 的容器异常检测 4. 赛题解析系列
363 12
|
12月前
|
SQL PyTorch 算法框架/工具
Intel技术专家:oneAPI 开放式加速计算|龙蜥大讲堂第114期
这次分享的主题是《oneAPI 开放式加速计算 龙蜥大讲堂第 114 期》的主要内容。主要分为四个部分: 1. 发展背景 2. 什么是 oneAPI 3. 产品应用 4. 总结展望
606 6
|
前端开发 JavaScript 搜索推荐
构建简易天气预报应用
【8月更文挑战第31天】在这篇文章中,我们将一起踏上制作一个简易天气预报应用的旅程。不同于常规的技术文章摘要,这里我们直接潜入主题的核心——如何从零开始,利用HTML、CSS和JavaScript构建一个功能完备的天气预报工具。我们会探索API的使用,理解异步编程概念,并实现一个响应式设计的用户界面。准备好迎接代码和创意的结合,让我们动手实践,共同打造属于你的天气小助手!
|
存储 缓存 负载均衡
带你认识DM 共享存储数据库集群
带你认识DM 共享存储数据库集群
503 3
|
Kubernetes 应用服务中间件 nginx
k8s(7)Deployment(部署)与ReplicaSet(副本集)
Deployment(部署)与ReplicaSet(副本集)
280 0
|
SQL 关系型数据库 MySQL
Liquibase----SQL格式通过update更新MySQL数据库
Liquibase----SQL格式通过update更新MySQL数据库
1226 0