The sixth article RxSwift uses MVVM to implement the login page

foreword

Since the fifth RxSwift login page Demo in this column is the original text of the Chinese version of RxSwift, it cannot meet the development needs of real projects, so the author decided to implement a login page based on actual project requirements. After reading this article, I hope that friends can develop it in practice. Get started with RxSwift in .

Login Page Requirements

page element

  1. app icon
  2. app name
  3. Mobile phone number input box
  4. Verification code input box
  5. Login agreement and checkbox

renderings

Require

  1. The mobile phone number is checked regularly, with 11 valid digits, and a maximum of 11 digits can be entered
  2. The verification code is 4 valid digits, and you can enter up to 4 digits
  3. Checkbox is unchecked by default
  4. The Get Verification Code button is gray by default and cannot be clicked
  5. When condition 1 is met, the Get Verification Code button is highlighted and clickable
  6. When the check box is not selected, click the Get Verification Code button, and it will prompt "Please read and agree to the agreement"
  7. When the check box is selected, click the Get Verification Code button, the button will start a 60-second countdown, and call the server interface to send the verification code. After the countdown ends, the clickable state will be restored
  8. When conditions 1 and 2 are met and the checkbox is selected, the login interface is called

accomplish

This page is relatively simple. The editor here directly uses xib for layout. I will explain the countdown and login protocol a little bit.

countdown related

Countdown UI

A view and a button are arranged around 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)
        }

countdown logic

It is mainly realized through timertwo countDownStoppedsequences. After clicking the button to obtain the verification code, timerit is responsible for the countdown, countDownStoppedindicating whether the countdown is over. The code is as follows:

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

Countdown complete code

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

Mixed layout of pictures and texts in the login agreement

UILabelImageText is used here . A UILabel supports mixed graphics and text. The editor has made a detailed introduction in the previous article. Interested friends can go and have a look. The code used this time is as follows

  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

Different events are represented by multiple sequences and the combination of multiple sequences. You only need to subscribe to the sequence and process related events when the sequence is pushed. The main sequence is as follows:

    //手机号长度限制序列
    let phoneTextMaxLengthObservable: Observable<String>
    //验证码长度限制序列
    let smsTextMaxLengthObservable: Observable<String>
    
    //验证码按钮是否可用序列
    let smsBtnEnableObservable: Observable<Bool>
    //一切准备好序列
    let everyThingValidObservable: Observable<Bool>

Mobile phone number length limit sequence

//手机号长度限制
phoneTextMaxLengthObservable = phone.map({ phoneNumber in
    return String(phoneNumber.prefix(11))
})

Captcha length limit

//验证码长度限制
smsTextMaxLengthObservable = smsCode.map({ phoneNumber in
    return String(phoneNumber.prefix(4))
})

Is the phone number valid?

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)In order to share the sequence, it was introduced at the end of the fifth part of this column. distinctUntilChanged()It is to prevent repeated pushes when the content of the input box remains unchanged. If not added, pushes will be made when the cursor is acquired and lost.

Is the verification code valid?


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)

The verification code button can be clicked to control


smsBtnEnableObservable = Observable.combineLatest(phoneVaild, smsCountDown).map({
    $0 && $1
}).share(replay: 1)

Login conditions


everyThingValidObservable = Observable.combineLatest(phoneVaild, smsValid, checkBox).map {$0 && $1 && $2}

ViewModel complete code


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

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

Complete code of 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("调用发送验证码接口")
    }
    
}

Summarize

The above is all the content of using RxSwift combined with MVVM to realize the login page. Since the editor is also learning, if there is something that needs to be optimized or wrong, please feel free to suggest it in the comment area. The complete demo of this article has been uploaded to GitHub. If you need it Friends can go to download and run to see.

RxSwift is really too powerful, and there are many operators that I don’t know very well, but the pace of my learning will not stop, and I will still follow the tutorials of the RxSwift Chinese documentation step by step. If you encounter difficulties in understanding, you need to For the practical part, the editor will try to share it as clearly as possible, come on! fighting! ! !

Guess you like

Origin juejin.im/post/7234466978911944764