返回 导航

Swift

hangge.com

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

作者:hangge | 2018-06-22 08:10
    这个用户注册样例我之前也做过,当时用的是 MVVM 架构(文章1文章2)。下面我使用 RxFeedback 来对其进行重构。

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,主视图控制器(ViewController)

下面是本文的重点了。与 MVVM 架构不同的是,我们不再需要定义 ViewModel,而是通过状态、事件、反馈循环来实现整个页面的业务逻辑分离。
import UIKit
import RxSwift
import RxCocoa
import RxFeedback

class ViewController: UIViewController {
    //用户名输入框、以及验证结果显示标签
    @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!
    
    let disposeBag = DisposeBag()
    
    //状态
    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? //注册结果
        
        //用户注册信息(只有开始注册状态下才有数据返回)
        var signupData: (username: String, password: String)? {
            return startSignup ? (username ?? "", password ?? "") : nil
        }
        
        //返回初始化状态
        static var empty: State {
            return State(username: nil, password: nil, repeatedPassword: nil,
                         usernameValidationResult: ValidationResult.empty,
                         passwordValidationResult: ValidationResult.empty,
                         repeatedPasswordValidationResult: ValidationResult.empty,
                         startSignup: false, signupResult: nil)
        }
    }
    
    //事件
    enum Event {
        case usernameChanged(String) //用户名输入
        case passwordChanged(String) //密码输入
        case repeatedPasswordChanged(String) //重复密码输入
        case usernameValidated(ValidationResult) //用户名验证结束
        case signup //用户注册
        case signupResponse(Bool) //注册响应
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //GitHub网络请求服务
        let networkService = GitHubNetworkService()
        
        //用户注册服务
        let signupService = GitHubSignupService()
        
        Driver.system(
            //初始状态
            initialState: State.empty,
            //各个事件对状态的改变
            reduce: { (state, event) -> State in
                switch event {
                case .usernameChanged(let value):
                    var result = state
                    result.username = value
                    result.signupResult = nil //防止弹出框重复弹出
                    return result
                case .passwordChanged(let value):
                    var result = state
                    result.password = value
                    //验证密码
                    result.passwordValidationResult =
                        signupService.validatePassword(result.password ?? "")
                    //验证密码重复输入
                    if result.repeatedPassword != nil {
                        result.repeatedPasswordValidationResult =
                            signupService.validateRepeatedPassword(
                                result.password ?? "",
                                repeatedPassword: result.repeatedPassword ?? ""
                        )
                    }
                    result.signupResult = nil
                    return result
                case .repeatedPasswordChanged(let value):
                    var result = state
                    result.repeatedPassword = value
                    //验证密码重复输入
                    result.repeatedPasswordValidationResult =
                        signupService.validateRepeatedPassword(
                            result.password ?? "",
                            repeatedPassword: result.repeatedPassword ?? ""
                        )
                    result.signupResult = nil
                    return result
                case .usernameValidated(let value):
                    var result = state
                    result.usernameValidationResult = value
                    result.signupResult = nil
                    return result
                case .signup:
                    var result = state
                    result.startSignup = true
                    result.signupResult = nil
                    return result
                case .signupResponse(let value):
                    var result = state
                    result.startSignup = false
                    result.signupResult = value
                    return result
                }
            },
            feedback:
                //UI反馈
                bind(self) { me, state in
                    //状态输出到页面控件上
                    let subscriptions = [
                        
                        //用户名验证结果绑定
                        state.map{ $0.usernameValidationResult }
                            .drive(me.usernameValidationOutlet.rx.validationResult),
                        //密码验证结果绑定
                        state.map{ $0.passwordValidationResult }
                            .drive(me.passwordValidationOutlet.rx.validationResult),
                        //重复密码验证结果绑定
                        state.map{ $0.repeatedPasswordValidationResult }
                            .drive(me.repeatedPasswordValidationOutlet.rx.validationResult),
                        //注册按钮是否可用
                        state.map{ $0.usernameValidationResult.isValid &&
                                   $0.passwordValidationResult.isValid &&
                                   $0.repeatedPasswordValidationResult.isValid}
                            .drive(onNext: { valid  in
                                me.signupOutlet.isEnabled = valid
                                me.signupOutlet.alpha = valid ? 1.0 : 0.3
                            }),
                        //活动指示器绑定
                        state.map{ $0.startSignup }
                            .drive(me.signInActivityIndicator.rx.isAnimating),
                        //注册结果显示
                        state.map{ $0.signupResult }
                            .filter{ $0 != nil }
                            .drive(onNext: { result  in
                                me.showMessage("注册" + (result! ? "成功" : "失败") + "!")
                            })
                    ]
                    //将 UI 事件变成Event输入到反馈循环里面去
                    let events = [
                        //用户名输入
                        me.usernameOutlet.rx.text.orEmpty.changed
                            .asSignal().map(Event.usernameChanged),
                        //密码输入
                        me.passwordOutlet.rx.text.orEmpty.changed
                            .asSignal().map(Event.passwordChanged),
                        //重复密码输入
                        me.repeatedPasswordOutlet.rx.text.orEmpty.changed
                            .asSignal().map(Event.repeatedPasswordChanged),
                        //注册按钮点击
                        me.signupOutlet.rx.tap
                            .asSignal().map{ _ in Event.signup },
                    ]
                    return Bindings(subscriptions: subscriptions, events: events)
                },
                //非UI的自动反馈(用户名验证)
                react(query: { $0.username }, effects: { username  in
                    return signupService.validateUsername(username)
                        .asSignal(onErrorRecover: { _ in .empty() })
                        .map(Event.usernameValidated)
                }),
                //非UI的自动反馈(用户注册)
                react(query: { $0.signupData }, effects: { (username, password)  in
                    return networkService.signup(username, password: password)
                        .asSignal(onErrorRecover: { _ in .empty() })
                        .map(Event.signupResponse)
                })
            )
            .drive()
            .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)
    }
}

附:代码的拆分优化

    上面样例中我们把 RxFeedBack 架构用到的 stateeventfeedback 等等都定义在 ViewController 中,这样功能一旦多些,ViewController 里的代码就会变得十分冗长,难以阅读。下面我对代码进行改成,拆分成各个独立的文件。

1,GitHubSignup+State.swift

StateEvent 以及相关的 reduce 都放在这里面。
import Foundation

//状态
struct GitHubSignupState {
    var username: String? //用户名
    var password: String? //密码
    var repeatedPassword: String? //再出输入密码
    var usernameValidationResult: ValidationResult //用户名验证结果
    var passwordValidationResult: ValidationResult //密码验证结果
    var repeatedPasswordValidationResult: ValidationResult //重复密码验证结果
    var startSignup: Bool //开始注册
    var signupResult: Bool? //注册结果
    
    //用户注册信息(只有开始注册状态下才有数据返回)
    var signupData: (username: String, password: String)? {
        return startSignup ? (username ?? "", password ?? "") : nil
    }
}

//事件
enum GitHubSignupEvent {
    case usernameChanged(String) //用户名输入
    case passwordChanged(String) //密码输入
    case repeatedPasswordChanged(String) //重复密码输入
    case usernameValidated(ValidationResult) //用户名验证结束
    case signup //用户注册
    case signupResponse(Bool) //注册响应
}

extension GitHubSignupState {
    //返回初始化状态
    static var empty: GitHubSignupState {
        return GitHubSignupState(username: nil, password: nil, repeatedPassword: nil,
                     usernameValidationResult: ValidationResult.empty,
                     passwordValidationResult: ValidationResult.empty,
                     repeatedPasswordValidationResult: ValidationResult.empty,
                     startSignup: false, signupResult: nil)
    }
    
    static func reduce(state: GitHubSignupState, event: GitHubSignupEvent
        , signupService: GitHubSignupService) -> GitHubSignupState {
            switch event {
            case .usernameChanged(let value):
                var result = state
                result.username = value
                result.signupResult = nil //防止弹出框重复弹出
                return result
            case .passwordChanged(let value):
                var result = state
                result.password = value
                //验证密码
                result.passwordValidationResult =
                    signupService.validatePassword(result.password ?? "")
                //验证密码重复输入
                if result.repeatedPassword != nil {
                    result.repeatedPasswordValidationResult =
                        signupService.validateRepeatedPassword(
                            result.password ?? "",
                            repeatedPassword: result.repeatedPassword ?? ""
                    )
                }
                result.signupResult = nil
                return result
            case .repeatedPasswordChanged(let value):
                var result = state
                result.repeatedPassword = value
                //验证密码重复输入
                result.repeatedPasswordValidationResult =
                    signupService.validateRepeatedPassword(
                        result.password ?? "",
                        repeatedPassword: result.repeatedPassword ?? ""
                )
                result.signupResult = nil
                return result
            case .usernameValidated(let value):
                var result = state
                result.usernameValidationResult = value
                result.signupResult = nil
                return result
            case .signup:
                var result = state
                result.startSignup = true
                result.signupResult = nil
                return result
            case .signupResponse(let value):
                var result = state
                result.startSignup = false
                result.signupResult = value
                return result
        }
    }
}

2,GitHubSignup+Feedback.swift

这里面放置与 UI 无关的反馈。
import Foundation
import RxSwift
import RxCocoa
import RxFeedback

struct GitHubSignupFeedback {
     //验证用户名
     static func validateUsername(signupService: GitHubSignupService)
        -> (Driver<GitHubSignupState>) -> Signal<GitHubSignupEvent> {
        
        let query:(GitHubSignupState) -> String?
            = { $0.username }
        
        let effects:(String) -> Signal<GitHubSignupEvent>
            = { return signupService.validateUsername($0)
                .asSignal(onErrorRecover: { _ in .empty() })
                .map(GitHubSignupEvent.usernameValidated) }
        
        return react(query: query, effects: effects)
    }
    
    //用户注册
    static func signup(networkService: GitHubNetworkService)
        -> (Driver<GitHubSignupState>) -> Signal<GitHubSignupEvent> {
            
            let query:(GitHubSignupState) -> (String, String)?
                = { $0.signupData }
            
            let effects:(String, String) -> Signal<GitHubSignupEvent>
                = { return networkService.signup($0, password: $1)
                    .asSignal(onErrorRecover: { _ in .empty() })
                    .map(GitHubSignupEvent.signupResponse) }
            
            return react(query: query, effects: effects)
    }
}

3,ViewController.swift

可以发现代码经过前面的提取之后,主视图控制器的代码变得简洁许多。
import UIKit
import RxSwift
import RxCocoa
import RxFeedback

class ViewController: UIViewController {
    //用户名输入框、以及验证结果显示标签
    @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!
    
    let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //GitHub网络请求服务
        let networkService = GitHubNetworkService()
        
        //用户注册服务
        let signupService = GitHubSignupService()
        
        //UI绑定
        let bindUI: (Driver<GitHubSignupState>) -> Signal<GitHubSignupEvent> =
            bind(self) { me, state in
            //状态输出到页面控件上
            let subscriptions = [
                //用户名验证结果绑定
                state.map{ $0.usernameValidationResult }
                    .drive(me.usernameValidationOutlet.rx.validationResult),
                //密码验证结果绑定
                state.map{ $0.passwordValidationResult }
                    .drive(me.passwordValidationOutlet.rx.validationResult),
                //重复密码验证结果绑定
                state.map{ $0.repeatedPasswordValidationResult }
                    .drive(me.repeatedPasswordValidationOutlet.rx.validationResult),
                //注册按钮是否可用
                state.map{ $0.usernameValidationResult.isValid &&
                    $0.passwordValidationResult.isValid &&
                    $0.repeatedPasswordValidationResult.isValid}
                    .drive(onNext: { valid  in
                        me.signupOutlet.isEnabled = valid
                        me.signupOutlet.alpha = valid ? 1.0 : 0.3
                    }),
                //活动指示器绑定
                state.map{ $0.startSignup }
                    .drive(me.signInActivityIndicator.rx.isAnimating),
                //注册结果显示
                state.map{ $0.signupResult }
                    .filter{ $0 != nil }
                    .drive(onNext: { result  in
                        me.showMessage("注册" + (result! ? "成功" : "失败") + "!")
                    })
            ]
            //将 UI 事件变成Event输入到反馈循环里面去
            let events = [
                //用户名输入
                me.usernameOutlet.rx.text.orEmpty.changed
                    .asSignal().map(GitHubSignupEvent.usernameChanged),
                //密码输入
                me.passwordOutlet.rx.text.orEmpty.changed
                    .asSignal().map(GitHubSignupEvent.passwordChanged),
                //重复密码输入
                me.repeatedPasswordOutlet.rx.text.orEmpty.changed
                    .asSignal().map(GitHubSignupEvent.repeatedPasswordChanged),
                //注册按钮点击
                me.signupOutlet.rx.tap
                    .asSignal().map{ _ in GitHubSignupEvent.signup },
                ]
            
            return Bindings(subscriptions: subscriptions, events: events)
        }
        
        Driver.system(
            //初始状态
            initialState: GitHubSignupState.empty,
            //各个事件对状态的改变
            reduce: { GitHubSignupState.reduce(state: $0, event: $1,
                                        signupService: signupService) },
            feedback:
                //UI反馈
                bindUI,
                //非UI的自动反馈(用户名验证)
                GitHubSignupFeedback.validateUsername(signupService: signupService),
                //非UI的自动反馈(用户注册)
                GitHubSignupFeedback.signup(networkService: networkService)
            )
            .drive()
            .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)

回到顶部