当前位置: > > > Swift - RxSwift的使用详解73(ReactorKit架构2:一个用户注册样例)

Swift - RxSwift的使用详解73(ReactorKit架构2:一个用户注册样例)

    这个用户注册样例我之前也做过,当时分别用的是 MVVM 架构(文章1文章2)以及 RxFeedback 架构(点击查看)。下面我使用 ReactorKit 来对其进行重构。

1,效果图

(1)默认“注册”按钮不可用,只有用户名、密码、再次输入密码三者都符合如下条件时才可用:
  • 输入用户名时会同步检查该用户名是否符合条件(只能为数字或字母),以及是否已存在(网络请求),并在输入框下方显示验证结果。(当连续输入时,如果上一次请求还会返未会自动取消,只处理最新请求。)
  • 输入密码时会检查密码是否符合条件(最少要 5 位),并在输入框下方显示验证结果。
  • 再次输入密码时会检查两个密码是否一致,并在输入框下方显示验证结果。
           

(2)当所有输入都符合条件时,点击“注册”按钮发起请求,并将结果弹出显示。同时在注册过程中按钮左侧会显示一个菊花状的网络请求指示器。
             

2,页面设计

(1)首先我们在 storyboard 中添加 3 个输入框、3 个文本标签,它们分别用于输入用户名、密码、确认密码,以及对应的验证结果显示。
(2)接着在界面最下方添加一个按钮用于注册。
(3)接着在注册按钮的左侧放置一个 Activity Indicator View,同时设置当其动画停止时自动隐藏。
(4)最后将这个 8 个 UI 控件与代码做 @IBOutlet 关联。

3,网络请求服务

我们首先将需要调用的网络请求:验证用户名是否存在,用户注册封装起来(GitHubNetworkService.swift),方便后面使用。(这里代码与之前那篇文章一样,没有变化)
import Foundation
import RxSwift

//GitHub网络请求服务
class GitHubNetworkService {
    
    //验证用户是否存在
    func usernameAvailable(_ username: String) -> Observable<Bool> {
        //通过检查这个用户的GitHub主页是否存在来判断用户是否存在
        let url = URL(string: "https://github.com/\(username.URLEscaped)")!
        let request = URLRequest(url: url)
        return URLSession.shared.rx.response(request: request)
            .map { pair in
                //如果不存在该用户主页,则说明这个用户名可用
                return pair.response.statusCode == 404
            }
            .catchErrorJustReturn(false)
    }
    
    //注册用户
    func signup(_ username: String, password: String) -> Observable<Bool> {
        //这里我们没有真正去发起请求,而是模拟这个操作(平均每3次有1次失败)
        let signupResult = arc4random() % 3 == 0 ? false : true
        return Observable.just(signupResult)
            .delay(1.5, scheduler: MainScheduler.instance) //结果延迟1.5秒返回
    }
}

//扩展String
extension String {
    //字符串的url地址转义
    var URLEscaped: String {
        return self.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""
    }
}

4,用户注册验证服务

(1)首先定义一个用于表示验证结果和信息的枚举(ValidationResult),后面我们会将它作为验证结果绑定到界面上。(这里代码与之前那篇文章一样,没有变化)
import UIKit

//验证结果和信息的枚举
enum ValidationResult {
    case validating  //正在验证中s
    case empty  //输入为空
    case ok(message: String) //验证通过
    case failed(message: String)  //验证失败
}

//扩展ValidationResult,对应不同的验证结果返回验证是成功还是失败
extension ValidationResult {
    var isValid: Bool {
        switch self {
        case .ok:
            return true
        default:
            return false
        }
    }
}

//扩展ValidationResult,对应不同的验证结果返回不同的文字描述
extension ValidationResult: CustomStringConvertible {
    var description: String {
        switch self {
        case .validating:
            return "正在验证..."
        case .empty:
            return ""
        case let .ok(message):
            return message
        case let .failed(message):
            return message
        }
    }
}

//扩展ValidationResult,对应不同的验证结果返回不同的文字颜色
extension ValidationResult {
    var textColor: UIColor {
        switch self {
        case .validating:
            return UIColor.gray
        case .empty:
            return UIColor.black
        case .ok:
            return UIColor(red: 0/255, green: 130/255, blue: 0/255, alpha: 1)
        case .failed:
            return UIColor.red
        }
    }
}

(2)接着将用户名、密码等各种需要用到的验证封装起来(GitHubSignupService.swift),方便后面使用。(返回的就是上面定义的 ValidationResult,这里代码与之前那篇文章一样,没有变化)
import UIKit
import RxSwift

//用户注册服务
class GitHubSignupService {
    
    //密码最少位数
    let minPasswordCount = 5
    
    //网络请求服务
    lazy var networkService = {
        return GitHubNetworkService()
    }()
    
    //验证用户名
    func validateUsername(_ username: String) -> Observable<ValidationResult> {
        //判断用户名是否为空
        if username.isEmpty {
            return .just(.empty)
        }
        
        //判断用户名是否只有数字和字母
        if username.rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) != nil {
            return .just(.failed(message: "用户名只能包含数字和字母"))
        }
        
        //发起网络请求检查用户名是否已存在
        return networkService
            .usernameAvailable(username)
            .map { available in
                //根据查询情况返回不同的验证结果
                if available {
                    return .ok(message: "用户名可用")
                } else {
                    return .failed(message: "用户名已存在")
                }
            }
            .startWith(.validating) //在发起网络请求前,先返回一个“正在检查”的验证结果
    }
    
    //验证密码
    func validatePassword(_ password: String) -> ValidationResult {
        let numberOfCharacters = password.count
        
        //判断密码是否为空
        if numberOfCharacters == 0 {
            return .empty
        }
        
        //判断密码位数
        if numberOfCharacters < minPasswordCount {
            return .failed(message: "密码至少需要 \(minPasswordCount) 个字符")
        }
        
        //返回验证成功的结果
        return .ok(message: "密码有效")
    }
    
    //验证二次输入的密码
    func validateRepeatedPassword(_ password: String, repeatedPassword: String)
        -> ValidationResult {
        //判断密码是否为空
        if repeatedPassword.count == 0 {
            return .empty
        }
        
        //判断两次输入的密码是否一致
        if repeatedPassword == password {
            return .ok(message: "密码有效")
        } else {
            return .failed(message: "两次输入的密码不一致")
        }
    }
}

5,对 UILabel 进行扩展(BindingExtensions.swift)

为了让 ValidationResult 能绑定到 label 上,我们要对 UILabel 进行扩展(这里代码与之前那篇文章一样,还是没有变化)
import UIKit
import RxSwift
import RxCocoa
 
//扩展UILabel
extension Reactive where Base: UILabel {
    //让验证结果(ValidationResult类型)可以绑定到label上
    var validationResult: Binder<ValidationResult> {
        return Binder(base) { label, result in
            label.textColor = result.textColor
            label.text = result.description
        }
    }
}

6,反应器(ViewReactor.swift)

这个就是本文的重点了,在这里我们将 Action 转换成 State
import RxSwift

final class ViewReactor: Reactor {
    
    //GitHub网络请求服务
    let networkService = GitHubNetworkService()
    
    //用户注册服务
    let signupService = GitHubSignupService()
    
    //代表用户行为
    enum Action {
        case usernameChanged(String) //用户名输入
        case passwordChanged(String) //密码输入
        case repeatedPasswordChanged(String) //重复密码输入
        case signup //用户注册
    }
    
    //代表状态变化
    enum Mutation {
        case setUsername(String) //设置用户名
        case setUsernameValidationResult(ValidationResult) //设置用户名验证结果
        case setPassword(String) //设置密码
        case setPasswordValidationResult(ValidationResult) //设置用户名验证结果
        case setRepeatedPassword(String) //设置重复密码
        case setRepeatedPasswordValidationResult(ValidationResult) //设置重复密码验证结果
        case setStartSignup(Bool) //设置注册状态(是否正在提交注册)
        case setSignupResult(Bool?) //设置注册结果
    }
    
    //代表页面状态
    struct State {        
        var username: String //用户名
        var password: String //密码
        var repeatedPassword: String //再出输入密码
        var usernameValidationResult: ValidationResult //用户名验证结果
        var passwordValidationResult: ValidationResult //密码验证结果
        var repeatedPasswordValidationResult: ValidationResult //重复密码验证结果
        var startSignup: Bool //开始注册
        var signupResult: Bool? //注册结果
    }
    
    //初始页面状态
    let initialState: State
    
    init() {
        self.initialState = State(
            username: "",
            password: "",
            repeatedPassword: "",
            usernameValidationResult: ValidationResult.empty,
            passwordValidationResult: ValidationResult.empty,
            repeatedPasswordValidationResult: ValidationResult.empty,
            startSignup: false,
            signupResult: nil
        )
    }
    
    //实现 Action -> Mutation 的转换
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        //用户名变化
        case let .usernameChanged(username):
            //依次执行下面2个状态变化动作
            return Observable.concat([
                //设置用户名
                Observable.just(Mutation.setUsername(username)),
                //设置用户名验证结果
                signupService.validateUsername(username)
                    //如果用户名再次变化,但上一次验证还未完成,上一次验证会自动取消
                    .takeUntil(self.action.filter(isUsernameChangedAction))
                    .map(Mutation.setUsernameValidationResult)
                ])
        //密码变化
        case let .passwordChanged(password):
            //依次执行下面2个状态变化动作
            return Observable.concat([
                //设置密码
                Observable.just(Mutation.setPassword(password)),
                //设置密码验证结果
                Observable.just( Mutation.setPasswordValidationResult(
                    signupService.validatePassword(password))
                ),
                //设置重复密码验证结果
                Observable.just( Mutation.setRepeatedPasswordValidationResult(
                    signupService.validateRepeatedPassword(
                        password,
                        repeatedPassword: self.currentState.repeatedPassword )
                    )
                )
                ])
        //重复密码变化
        case let .repeatedPasswordChanged(repeatedPassword):
            //依次执行下面2个状态变化动作
            return Observable.concat([
                //设置重复密码
                Observable.just(Mutation.setRepeatedPassword(repeatedPassword)),
                //设置重复密码验证结果
                Observable.just( Mutation.setRepeatedPasswordValidationResult(
                    signupService.validateRepeatedPassword(
                        self.currentState.password,
                        repeatedPassword: repeatedPassword )
                    )
                )
                ])
        //注册按钮点击
        case .signup:
            //依次执行下面4个状态变化动作
            return Observable.concat([
                //先清空之前的结果
                Observable.just(Mutation.setSignupResult(nil)),
                //开始注册
                Observable.just(Mutation.setStartSignup(true)),
                //设置注册结果
                networkService.signup(self.currentState.username,
                                      password: self.currentState.password)
                    .map(Mutation.setSignupResult),
                //注册结束
                Observable.just(Mutation.setStartSignup(false))
                ])
        }
    }
    
    //实现 Mutation -> State 的转换
    func reduce(state: State, mutation: Mutation) -> State {
        //从旧状态那里复制一个新状态
        var state = state
        
        //根据状态变化设置响应的状态值
        switch mutation {
        //设置用户名
        case let .setUsername(username):
            state.username = username
            state.signupResult = nil //信息一有变化则清除注册结果
        //设置用户名验证结果
        case let .setUsernameValidationResult(validationResult):
            state.usernameValidationResult = validationResult
        //设置密码
        case let .setPassword(password):
            state.password = password
            state.signupResult = nil //信息一有变化则清除注册结果
        //设置密码验证结果
        case let .setPasswordValidationResult(validationResult):
            state.passwordValidationResult = validationResult
        //设置重复密码
        case let .setRepeatedPassword(repeatedPassword):
            state.repeatedPassword = repeatedPassword
            state.signupResult = nil //信息一有变化则清除注册结果
        //设置重复密码验证结果
        case let .setRepeatedPasswordValidationResult(validationResult):
            state.repeatedPasswordValidationResult = validationResult
        //设置注册状态
        case let .setStartSignup(value):
            state.startSignup = value
        //设置注册结果
        case let .setSignupResult(value):
            state.signupResult = value
        }
        
        //返回新状态
        return state
    }
    
    //判断当前的 action 是否是用户名输入
    private func isUsernameChangedAction(_ action: Action) -> Bool {
        if case .usernameChanged = action {
            return true
        } else {
            return false
        }
    }
}

7,视图(ViewController.swift)

主视图控制器里的代码就很简单了,只需要将页面上的用户行为绑定到 reactor 里的 action 上,同时将 reactor 里的 state 绑定到页面 UI 控件上即可。
import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController, StoryboardView {
    //用户名输入框、以及验证结果显示标签
    @IBOutlet weak var usernameOutlet: UITextField!
    @IBOutlet weak var usernameValidationOutlet: UILabel!
    
    //密码输入框、以及验证结果显示标签
    @IBOutlet weak var passwordOutlet: UITextField!
    @IBOutlet weak var passwordValidationOutlet: UILabel!
    
    //重复密码输入框、以及验证结果显示标签
    @IBOutlet weak var repeatedPasswordOutlet: UITextField!
    @IBOutlet weak var repeatedPasswordValidationOutlet: UILabel!
    
    //注册按钮
    @IBOutlet weak var signupOutlet: UIButton!
    
    //注册时的活动指示器
    @IBOutlet weak var signInActivityIndicator: UIActivityIndicatorView!
    
    var disposeBag = DisposeBag()
    
   
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //设置reactor,会自动触发bind()方法
        self.reactor = ViewReactor()
    }
    
    //处理绑定事件(该方法会在 self.reactor 变化时自动触发)
    func bind(reactor: ViewReactor) {
        
        //Action(实现 View -> Reactor 的绑定)
        usernameOutlet.rx.text.orEmpty.changed  //用户名输入框文字改变事件
            .throttle(0.3, scheduler: MainScheduler.instance)
            .distinctUntilChanged()
            .map(Reactor.Action.usernameChanged)  //转换为 Action.usernameChanged
            .bind(to: reactor.action)  //绑定到 reactor.action
            .disposed(by: disposeBag)
        
        passwordOutlet.rx.text.orEmpty.changed  //密码输入框文字改变事件
            .distinctUntilChanged()
            .map(Reactor.Action.passwordChanged)  //转换为 passwordChanged
            .bind(to: reactor.action)  //绑定到 reactor.action
            .disposed(by: disposeBag)
        
        repeatedPasswordOutlet.rx.text.orEmpty.changed  //重复密码输入框文字改变事件
            .distinctUntilChanged()
            .map(Reactor.Action.repeatedPasswordChanged)  //转换为 repeatedPasswordChanged
            .bind(to: reactor.action)  //绑定到 reactor.action
            .disposed(by: disposeBag)
        
        signupOutlet.rx.tap //注册按钮点击事件
            .map{ Reactor.Action.signup }  //转换为 signup
            .bind(to: reactor.action)  //绑定到 reactor.action
            .disposed(by: disposeBag)
        
        // State(实现 Reactor -> View 的绑定)
        reactor.state.map { $0.usernameValidationResult }  //得到最新用户名验证结果
            .bind(to: usernameValidationOutlet.rx.validationResult)  //绑定到文本标签上
            .disposed(by: disposeBag)
        
        reactor.state.map { $0.passwordValidationResult }  //得到最新密码验证结果
            .bind(to: passwordValidationOutlet.rx.validationResult)  //绑定到文本标签上
            .disposed(by: disposeBag)
        
        reactor.state.map { $0.repeatedPasswordValidationResult }  //得到最新重复密码验证结果
            .bind(to: repeatedPasswordValidationOutlet.rx.validationResult)//绑定到文本标签上
            .disposed(by: disposeBag)
        
        //注册按钮是否可用
        reactor.state.map{ $0.usernameValidationResult.isValid &&
            $0.passwordValidationResult.isValid &&
            $0.repeatedPasswordValidationResult.isValid }
            .subscribe(onNext: { [weak self] valid in
                self?.signupOutlet.isEnabled = valid
                self?.signupOutlet.alpha = valid ? 1.0 : 0.3
            })
            .disposed(by: disposeBag)
        
        //活动指示器绑定
        reactor.state.map { $0.startSignup }
            .bind(to: signInActivityIndicator.rx.isAnimating)
            .disposed(by: disposeBag)
        
        //注册结果显示
        reactor.state.map { $0.signupResult }
            .filter{ $0 != nil }
            .subscribe(onNext: { [weak self] result in
                self?.showMessage("注册" + (result! ? "成功" : "失败") + "!")
            })
            .disposed(by: disposeBag)
    }
    
    //详细提示框
    func showMessage(_ message: String) {
        let alertController = UIAlertController(title: nil,
                                                message: message, preferredStyle: .alert)
        let okAction = UIAlertAction(title: "确定", style: .cancel, handler: nil)
        alertController.addAction(okAction)
        self.present(alertController, animated: true, completion: nil)
    }
}
评论0