前语
由于本专栏第五篇 RxSwift登录页Demo 是RxSwift中文版的原文,无法满意真正项目中的开发需求,所以笔者决定以实际项目需求完成一个登录页面,看完本篇期望小伙伴们能够在实际开发中开始运用RxSwift。
登录页要求
页面元素
- app图标
- app名称
- 手机号输入框
- 验证码输入框
- 登录协议及复选框
效果图
要求
- 手机号做正则校验,11位有用数字,最多输入11位
- 验证码为4位有用数字,最多输入4位
- 复选框默认不选中
- 获取验证码按钮默认灰色,不行点击
- 条件1满意时,获取验证码按钮高亮显现,支撑点击
- 复选框未选中时,点击获取验证码按钮,提示“请阅览并赞同协议”
- 复选框选中时,点击获取验证码按钮,按钮开启60秒倒计时,且调用服务端接口发送验证码,倒计时完毕,恢复可点击状况
- 条件1、2满意且复选框选中时,调用登录接口
完成
此页面比较简单,小编这儿直接选用xib进行布局了,略微对倒计时和登录协议做一下讲解
倒计时相关
倒计时UI
一个view和一个button左右选用SnapKit进行布局
let line = UIView()
line.backgroundColor = .darkGray
addSubview(line)
addSubview(smsBtn)
line.snp.makeConstraints { make in
make.top.left.equalTo(5)
make.width.equalTo(1)
make.centerY.equalToSuperview()
}
smsBtn.snp.makeConstraints { make in
make.left.equalTo(line.snp.right).offset(0)
make.top.bottom.right.equalToSuperview()
make.width.equalTo(120)
}
倒计时逻辑
首要是经过timer
和countDownStopped
两个序列完成,获取验证码按钮点击之后,timer
负责倒计时,countDownStopped
表示倒计时是否完毕,代码如下:
let countDownStopped = BehaviorRelay(value: true)
var leftTime = countDownSeconds
let timer = Observable<Int>.interval(RxTimeInterval.seconds(1), scheduler: MainScheduler.instance)
func countdownTime(){
// 开始倒计时
self.countDownStopped.accept(false)
timer.take(until: countDownStopped.asObservable().filter{$0})
.observe(on: MainScheduler.asyncInstance)
.subscribe(onNext: { [weak self](event) in
guard let self = self else {
return
}
self.leftTime -= 1
/// UI操作
self.smsBtn.setTitle("\(self.leftTime)秒后从头获取", for: .normal)
if (self.leftTime == 0) {
print("倒计时完毕")
self.countDownStopped.accept(true)
self.leftTime = countDownSeconds
}
}, onError: nil )
.disposed(by: disposeBag)
}
倒计时完好代码
//
// JZLoginSMSRightView.swift
//
//
// Created by 陈武琦 on 2023/4/20.
//
import UIKit
import SnapKit
import RxSwift
import RxRelay
private let countDownSeconds: Int = 60
class JZLoginSMSRightView: UIView {
var disposeBag = DisposeBag()
let countDownStopped = BehaviorRelay(value: true)
var leftTime = countDownSeconds
let timer = Observable<Int>.interval(RxTimeInterval.seconds(1), scheduler: MainScheduler.instance)
public lazy var smsBtn = {
let btn = UIButton(frame: CGRect(x: 0, y: 0, width: 160, height: 50))
btn.setTitle("获取验证码", for: .normal)
btn.setTitleColor(.red, for: .normal)
btn.setTitleColor(.gray, for: .disabled)
btn.titleLabel?.font = UIFont.systemFont(ofSize: 14)
return btn
}()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
let line = UIView()
line.backgroundColor = .darkGray
addSubview(line)
addSubview(smsBtn)
line.snp.makeConstraints { make in
make.top.left.equalTo(5)
make.width.equalTo(1)
make.centerY.equalToSuperview()
}
smsBtn.snp.makeConstraints { make in
make.left.equalTo(line.snp.right).offset(0)
make.top.bottom.right.equalToSuperview()
make.width.equalTo(120)
}
countDownStopped.subscribe {[weak self] stoped in
guard let self = self else {
return
}
if stoped {
self.smsBtn.setTitle("获取验证码", for: .normal)
}
}.disposed(by: disposeBag)
}
func countdownTime(){
// 开始倒计时
self.countDownStopped.accept(false)
timer.take(until: countDownStopped.asObservable().filter{$0})
.observe(on: MainScheduler.asyncInstance)
.subscribe(onNext: { [weak self](event) in
guard let self = self else {
return
}
self.leftTime -= 1
/// UI操作
self.smsBtn.setTitle("\(self.leftTime)秒后从头获取", for: .normal)
if (self.leftTime == 0) {
print("倒计时完毕")
self.countDownStopped.accept(true)
self.leftTime = countDownSeconds
/// UI操作
}
}, onError: nil )
.disposed(by: disposeBag)
}
}
登录协议图文混排
这儿运用了UILabelImageText,一个UILabel支撑图文混排,小编在前面的文章里做过详细的介绍,有兴趣的小伙伴能够去看看,本次运用代码如下
func setupAgreement() {
agreeL.imageText(normalImage: UIImage(named: "common_icon_unselected"), selectedImage: UIImage(named: "common_icon_selected"), content: " 我已阅览并赞同《用户协议》和《隐私协议》", font: UIFont.systemFont(ofSize: 12), largeFont: UIFont.systemFont(ofSize: 20), alignment: .left)
agreeL.setImageCallBack {[weak self] in
Toast("点击图标")
self?.agreementSelected.onNext(self?.agreeL.selected ?? false)
}
agreeL.setSubstringCallBack(substring: "《用户协议》", color: .gray) {
Toast("点击《用户协议》")
}
agreeL.setSubstringCallBack(substring: "《隐私协议》", color: .gray) {
Toast("点击《隐私协议》")
}
}
ViewModel
经过多个序列及多个序列的联合来表示不同的事情,只需求订阅序列,在序列推送时处理相关事情即可,首要序列如下:
//手机号长度约束序列
let phoneTextMaxLengthObservable: Observable<String>
//验证码长度约束序列
let smsTextMaxLengthObservable: Observable<String>
//验证码按钮是否可用序列
let smsBtnEnableObservable: Observable<Bool>
//一切准备好序列
let everyThingValidObservable: Observable<Bool>
手机号长度约束序列
//手机号长度约束
phoneTextMaxLengthObservable = phone.map({ phoneNumber in
return String(phoneNumber.prefix(11))
})
验证码长度约束
//验证码长度约束
smsTextMaxLengthObservable = smsCode.map({ phoneNumber in
return String(phoneNumber.prefix(4))
})
手机号是否有用
let phoneVaild = phone.map {
let regex = "^1[3456789]\\d{9}$"
let predicate = NSPredicate(format: "SELF MATCHES %@", regex)
return predicate.evaluate(with: $0) && $0.count == phoneMaxLength
}.distinctUntilChanged().share(replay: 1)
share(replay: 1)
是为了将序列共享,在本专栏第五篇末尾做过介绍。
distinctUntilChanged()
是为了避免输入框内容不变时重复推送,如果不加,当光标获取和失去时都会推送。
验证码是否有用
let smsValid = smsCode.map {
let regex = "^\\d{4}$"
let predicate = NSPredicate(format: "SELF MATCHES %@", regex)
return predicate.evaluate(with: $0) && $0.count == smsMaxLength
}.distinctUntilChanged().share(replay: 1)
验证码按钮可点击操控
smsBtnEnableObservable = Observable.combineLatest(phoneVaild, smsCountDown).map({
$0 && $1
}).share(replay: 1)
登录条件
everyThingValidObservable = Observable.combineLatest(phoneVaild, smsValid, checkBox).map {$0 && $1 && $2}
ViewModel完好代码
//
// JZLoginViewModel.swift
//
//
// Created by 陈武琦 on 2023/4/26.
//
import Foundation
import RxSwift
//手机号长度
let phoneMaxLength = 11
//验证码长度
let smsMaxLength = 4
class JZLoginViewModel {
//手机号长度约束序列
let phoneTextMaxLengthObservable: Observable<String>
//验证码长度约束序列
let smsTextMaxLengthObservable: Observable<String>
//验证码按钮是否可用序列
let smsBtnEnableObservable: Observable<Bool>
//一切准备好序列
let everyThingValidObservable: Observable<Bool>
init(
phone: Observable<String>,
smsCode: Observable<String>,
smsCountDown: Observable<Bool>,
checkBox:Observable<Bool>,
disposeBag:DisposeBag) {
//手机号是否有用
let phoneVaild = phone.map {
let regex = "^1[3456789]\\d{9}$"
let predicate = NSPredicate(format: "SELF MATCHES %@", regex)
return predicate.evaluate(with: $0) && $0.count == phoneMaxLength
}.distinctUntilChanged().share(replay: 1)
//验证码是否有用
let smsValid = smsCode.map {
let regex = "^\\d{4}$"
let predicate = NSPredicate(format: "SELF MATCHES %@", regex)
return predicate.evaluate(with: $0) && $0.count == smsMaxLength
}.distinctUntilChanged().share(replay: 1)
//手机号长度约束
phoneTextMaxLengthObservable = phone.map({ phoneNumber in
return String(phoneNumber.prefix(11))
})
//验证码长度约束
smsTextMaxLengthObservable = smsCode.map({ phoneNumber in
return String(phoneNumber.prefix(4))
})
//验证码按钮可点击操控
smsBtnEnableObservable = Observable.combineLatest(phoneVaild, smsCountDown).map({
$0 && $1
}).share(replay: 1)
everyThingValidObservable = Observable.combineLatest(phoneVaild, smsValid, checkBox).map {$0 && $1 && $2}
}
}
LoginViewController
绑定viewmodel
func bindViewModel() {
let phoneObservable = phoneTextField.rx.text.orEmpty.asObservable()
let smsObservable = smsCodeTextField.rx.text.orEmpty.asObservable()
let smsCountDownObservable = smsRightView.countDownStopped.asObservable()
let checkBoxObservable = agreementSelected.asObservable()
let viewModel = JZLoginViewModel(phone: phoneObservable,
smsCode: smsObservable,
smsCountDown: smsCountDownObservable,
checkBox: checkBoxObservable,
disposeBag:disposeBag)
//操控手机号长度
viewModel.phoneTextMaxLengthObservable
.bind(to: phoneTextField.rx.text)
.disposed(by: disposeBag)
//操控验证码长度
viewModel.smsTextMaxLengthObservable
.bind(to: smsCodeTextField.rx.text)
.disposed(by: disposeBag)
//操控按钮是否可点击
viewModel.smsBtnEnableObservable
.bind(to: smsRightView.smsBtn.rx.isEnabled)
.disposed(by: disposeBag)
//订阅按钮点击
smsRightView.smsBtn.rx.tap
.withLatestFrom(agreementSelected.asObservable())
.subscribe {[weak self] checked in
guard let self = self else {
return
}
if checked {
self.sendSMSCode()
self.smsRightView.countdownTime()
}else {
Toast("请阅览并赞同协议")
}
}.disposed(by: disposeBag)
//订阅登录
viewModel.everyThingValidObservable.subscribe {[weak self] valid in
if valid { //满意登录条件
self?.login()
}
}.disposed(by: disposeBag)
}
LoginViewController完好代码
//
// JZLoginViewController.swift
// DreamVideo
//
// Created by 陈武琦 on 2023/4/20.
//
import UIKit
import SnapKit
import RxSwift
import MBProgressHUD
import UILabelImageText
import RxCocoa
class JZLoginViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var phoneTextField: UITextField!
@IBOutlet weak var smsCodeTextField: UITextField!
@IBOutlet weak var agreeL: UILabel!
private lazy var smsRightView: JZLoginSMSRightView = {
let rightView = JZLoginSMSRightView()
return rightView
}()
var disposeBag = DisposeBag()
let agreementSelected = BehaviorSubject<Bool>(value: false)
override func viewDidLoad() {
super.viewDidLoad()
title = "登录"
imageView.layer.cornerRadius = 5
imageView.layer.masksToBounds = true
smsCodeTextField.rightView = smsRightView
smsCodeTextField.rightViewMode = .always;
setupAgreement()
bindViewModel()
}
func setupAgreement() {
agreeL.imageText(normalImage: UIImage(named: "common_icon_unselected"), selectedImage: UIImage(named: "common_icon_selected"), content: " 我已阅览并赞同《用户协议》和《隐私协议》", font: UIFont.systemFont(ofSize: 12), largeFont: UIFont.systemFont(ofSize: 20), alignment: .left)
agreeL.setImageCallBack {[weak self] in
Toast("点击图标")
self?.agreementSelected.onNext(self?.agreeL.selected ?? false)
}
agreeL.setSubstringCallBack(substring: "《用户协议》", color: .gray) {
Toast("点击《用户协议》")
}
agreeL.setSubstringCallBack(substring: "《隐私协议》", color: .gray) {
Toast("点击《隐私协议》")
}
}
}
/// 绑定ViewModel
extension JZLoginViewController {
func bindViewModel() {
let phoneObservable = phoneTextField.rx.text.orEmpty.asObservable()
let smsObservable = smsCodeTextField.rx.text.orEmpty.asObservable()
let smsCountDownObservable = smsRightView.countDownStopped.asObservable()
let checkBoxObservable = agreementSelected.asObservable()
let viewModel = JZLoginViewModel(phone: phoneObservable,
smsCode: smsObservable,
smsCountDown: smsCountDownObservable,
checkBox: checkBoxObservable,
disposeBag:disposeBag)
//操控手机号长度
viewModel.phoneTextMaxLengthObservable
.bind(to: phoneTextField.rx.text)
.disposed(by: disposeBag)
//操控验证码长度
viewModel.smsTextMaxLengthObservable
.bind(to: smsCodeTextField.rx.text)
.disposed(by: disposeBag)
//操控按钮是否可点击
viewModel.smsBtnEnableObservable
.bind(to: smsRightView.smsBtn.rx.isEnabled)
.disposed(by: disposeBag)
//订阅按钮点击
smsRightView.smsBtn.rx.tap
.withLatestFrom(agreementSelected.asObservable())
.subscribe {[weak self] checked in
guard let self = self else {
return
}
if checked {
self.sendSMSCode()
self.smsRightView.countdownTime()
}else {
Toast("请阅览并赞同协议")
}
}.disposed(by: disposeBag)
//订阅登录
viewModel.everyThingValidObservable.subscribe {[weak self] valid in
if valid {
self?.login()
}
}.disposed(by: disposeBag)
}
}
/// 网络请求
extension JZLoginViewController {
func login() {
guard let phone = phoneTextField.text, let sms = smsCodeTextField.text else {return}
let hud = MBProgressHUD.showAdded(to: view, animated: true)
DispatchQueue.main.asyncAfter(deadline: .now()+2) {
print("phone:" + phone + " sms:" + sms)
hud.label.text = "登录成功"
hud.hide(animated: true, afterDelay: 1)
}
}
func sendSMSCode() {
print("调用发送验证码接口")
}
}
总结
以上是运用RxSwift结合MVVM完成登录页面的一切内容,由于小编也在学习中,如果有需求优化或者不对的当地,欢迎小伙伴们谈论区提出来,本篇完好的demo已上传GitHub,有需求的小伙伴能够去下载运转看看。
RxSwift真的是太强壮了,有许多操作符小编也不是很清楚,不过小编学习的步伐不会停,后续依旧会跟随着RxSwift中文文档的教程一步步学习,遇到欠好理解,需求实操的当地,小编会尽量的整理解分享出来,加油!fighting!!!