iOS-Rx

RxSwift官方实例二(MVVM)

2020-08-07  本文已影响0人  酒茶白开水

代码下载

搭建UI

如下图所示,搭建好UI并连线:


构建UI

弹框

由于弹框属于View层,但是又得在ViewModel中使用,这违背了MVVM模式中ViewModel不能引用View的限制。所以通过协议来解决这个问题,在ViewModel层中定义如下协议:

protocol WireFrame {
    /// 弹框
    /// - Parameters:
    ///   - title: 标题
    ///   - message: 信息
    ///   - cancelAction: 取消按钮
    ///   - actions: 其他按钮数组
    ///   - animated: 是否带动画
    ///   - completion: 完成闭包
    func promptFor<Action: CustomStringConvertible>(_ title: String, message: String, cancelAction: Action, actions: [Action]?, animated: Bool, completion: (() -> Void)?) -> Observable<Action>
}

在View层中定义一个DefaultWireFrame类实现上面的协议:

class DefaultWireFrame: WireFrame {
    func promptFor<Action: CustomStringConvertible>(_ title: String, message: String, cancelAction: Action, actions: [Action]? = nil, animated: Bool = true, completion: (() -> Void)? = nil) -> Observable<Action> {
        return Observable.create({ (observer) -> Disposable in
            let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: cancelAction.description, style: .cancel, handler: { (_) in
                observer.onNext(cancelAction)
            }))
            
            if let  actions = actions {
                for action in actions {
                    alert.addAction(UIAlertAction(title: action.description, style: .default, handler: { (_) in
                        observer.onNext(action)
                    }))
                }
            }
            
            DefaultWireFrame.rootViewController().present(alert, animated: animated, completion: completion)
            
            return Disposables.create { [weak alert] in
                alert?.dismiss(animated: animated, completion: nil)
            }
        })
    }
}

代码分析:

  1. 使用create操作符创建Observable序列
  2. 创建UIAlertController并添加UIAlertAction,在所有UIAlertAction的处理事件中用onNext发出元素
  3. 使用present弹出弹框,构建并返回资源清理对象Disposable,清理资源时使用dismiss关闭弹框

Model层

model层表示程序的状态——业务逻辑

业务逻辑

在Model层中创建ValidationResult结构用以表示用户数据是否有效:

/// 有效结果
enum ValidationResult {
    case ok(message: String)// 有效
    case empty // 空
    case validating // 验证中
    case failed(message: String) // 失败
}

在Model层为Sting类型扩展扩展一个URLEscaped属性用以提供URL编码的String:

extension String {
    var URLEscaped: String {
        return self.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""
    }
}

Model层暴露服务给ViewModel,在Model层定义如下协议,暴露API接口:

/// Github网络服务接口
protocol GithubApi {
    /// 用户名是否有效
    /// - Parameter username: 用户名
    func usernameAvailable(_ username: String) -> Observable<Bool>
    
    /// 注册
    /// - Parameters:
    ///   - username: 用户名
    ///   - password: 密码
    func signup(_ username: String, password: String) -> Observable<Bool>
}


/// GitHub数据是否有效接口
protocol GitHubValidationService {
    /// 判断用户名是否有效
    /// - Parameter username: 用户名
    func validateUsername(_ username: String) -> Observable<ValidationResult>
    
    /// 判断密码是否有效
    /// - Parameter password: 密码
    func validatePassword(_ password: String) -> ValidationResult
    
    /// 判断二次输入的密码是否有效
    /// - Parameters:
    ///   - password: 第一次输入的密码
    ///   - repeatPassword: 第二次输入的密码
    func validateRepeatedPassword(_ password: String, repeatPassword: String) -> ValidationResult
}

在Model层定义GitHubDefaultAPI类遵守GithubApi协议所暴露的接口:

class GitHubDefaultAPI: GithubApi {
    let session: URLSession
    init(_ session: URLSession) {
        self.session = session
    }
    
    static let shareApi = GitHubDefaultAPI(URLSession.shared)
    
    /// 检验用户名是否有效
    /// - Parameter username: 用户名
    func usernameAvailable(_ username: String) -> Observable<Bool> {
        let url = URL(string: "https://github.com/\(username.URLEscaped)")!
        let request = URLRequest(url: url)
        
        // 直接获取github用户数据
        return session.rx.response(request: request).map({ (pair) -> Bool in
            // 如果404错误则用户名有效,说明没有被使用
            return pair.response.statusCode == 404
        }).catchErrorJustReturn(false)
    }
    
    /// 注册
    /// - Parameters:
    ///   - username: 用户名
    ///   - password: 密码
    func signup(_ username: String, password: String) -> Observable<Bool> {
        // 四分之一的几率失败
        let result = arc4random()%4 == 0 ? false : true
        // 模拟网络请求
        return Observable.just(result).delay(.milliseconds(1000 + Int(arc4random()%3000)), scheduler: MainScheduler.instance)
    }
}

代码分析:

  1. usernameAvailable序列使用URLSession请求github的对应用户,如果成功证明用户名已经存在。使用catchErrorJustReturn操作符捕捉网络错误
  2. signup序列是模拟的网络请求,通过just操作符构建一个随机元素,delay操作符模拟一段随机请求时间

在Model层定义GitHubDefaultValidationService类遵守GitHubValidationService协议所暴露的接口:

/// 服务
class GitHubDefaultValidationService: GitHubValidationService {
    /// 接口
    let api: GithubApi
    
    /// 密码最少位数
    let minPasswordCount = 6
    /// 密码最大位数
    let maxPasswordCount = 24
    
    init(_ api: GithubApi) {
        self.api = api
    }
    
    func validateUsername(_ username: String) -> Observable<ValidationResult> {
        if username.isEmpty {
            return .just(.empty)
        }
        
        let loadingValue = ValidationResult.validating
        
        return api.usernameAvailable(username).map({ (vailable) -> ValidationResult in
            if vailable {
                return .ok(message: "用户名有效")
            } else {
                return .failed(message: "用户名无效")
            }
        }).startWith(loadingValue)
    }
    
    func validatePassword(_ password: String) -> ValidationResult {
        if password.isEmpty {
            return .empty
        }
        
        if password.count < minPasswordCount {
            return .failed(message: "密码不能小于\(minPasswordCount)6位")
        } else if password.count > maxPasswordCount {
            return .failed(message: "密码不能大于于\(maxPasswordCount)位")
        }
        
        return .ok(message: "密码可用")
    }
    
    func validateRepeatedPassword(_ password: String, repeatPassword: String) -> ValidationResult {
        if repeatPassword.isEmpty {
            return .empty
        }
        
        if repeatPassword == password {
            return .ok(message: "密码相同")
        } else {
            return .failed(message: "密码不同")
        }
    }
}

代码分析:

  1. 使用常量minPasswordCount、maxPasswordCount定义好密码的最小和最大长度
  2. validateUsername序列,用户名为空时返回空序列,否则请求网络将结果使用map操作符转化为ValidationResult类型,并使用startWith操作符设置一个初始元素
  3. validatePassword序列跟validateUsername序列类似,主要差别就是密码是根据长度来判断有效性的
  4. validateRepeatedPassword序列是通过两次密码是否相同来判断有效性的

活动指示器

首先定义一个ActivityToken:

/// 活动令牌
struct ActivityToken<E>: ObservableConvertibleType, Disposable {
    let _source: Observable<E>
    let _dispose: Cancelable
    
    init(source: Observable<E>, disposeAction: @escaping () -> Void) {
        _source = source
        _dispose = Disposables.create(with: disposeAction)
    }
    func asObservable() -> Observable<E> {
        return _source
    }
    
    func dispose() {
        _dispose.dispose()
    }
}

ActivityToken遵守Disposable、ObservableConvertibleType协议,也就是说ActivityToken既是Disposable也是Observable。实际上是用来存储一个Observable和Disposable清理资源的闭包。

定义ActivityIndicator:

/// 活动指示器
class ActivityIndicator: SharedSequenceConvertibleType {
    typealias Element = Bool
    typealias SharingStrategy = DriverSharingStrategy
    
    /// 锁
    let lock = NSRecursiveLock()
    /// 计数序列
    let relay = BehaviorRelay(value: 0)
    /// 加载序列
    let loading: SharedSequence<SharingStrategy, Bool>
    
    init() {
        loading = relay.asDriver().map({ $0 > 0 }).distinctUntilChanged()
    }
    
    /// 增量计数
    func increment() {
        lock.lock()
        relay.accept(relay.value + 1)
        lock.unlock()
    }
    /// 减量计数
    func decrement() {
        lock.lock()
        relay.accept(relay.value - 1)
        lock.unlock()
    }
    
    /// 跟踪活动
    /// - Parameter source: 源序列
    func trackActivityOfObservable<Source: ObservableConvertibleType>(_ source: Source) -> Observable<Source.Element> {
        return Observable.using({ [weak self] () -> ActivityToken<Source.Element> in
            // 增量计数
            self?.increment()
            // 返回一个Disposable
            return ActivityToken(source: source.asObservable(), disposeAction: self?.decrement ?? {})
        }, observableFactory: { (t) in
            // 返回一个序列
            t.asObservable()
        })
    }
    
    /// 遵守协议
    func asSharedSequence() -> SharedSequence<DriverSharingStrategy, Bool> {
        return loading
    }
}

ActivityIndicator的实现思想类似于内存管理中的引用计数,通过increment/decrement这两个函数来增/减计数的值,再使用BehaviorRelay把这些计数值作为元素发送出来,最后通过map操作符将元素转换为转化为BOOL类型的序列。

trackActivityOfObservable函数实现:

  1. 该函数接收一个Observable序列作为参数
  2. 执行增量计数函数
  3. 把减量计数函数和参数序列包装到ActivityToken中
  4. 使用using操作符,把前面的ActivityToken作为resourceFactory(序列完成需要清理的资源)参数。保证参数序列完成时,清理资源的同时执行减量计数函数
  5. 返回结果序列

扩展ObservableConvertibleType协议,方便Observable序列记录活动状态:

extension ObservableConvertibleType {
    func trackActivity(_ indicator: ActivityIndicator) -> Observable<Element> {
        return indicator.trackActivityOfObservable(self)
    }
}

使用Observeble的实现方案

MVVM模式的核心是ViewModel,它是一种特殊的model类型,用于表示程序的UI状态,包含描述每个UI控件的状态,所有的UI逻辑都在ViewModel中。

实际上ViewModel暴露属性来表示UI状态,它同样暴露命令来表示UI操作(通常是方法)。ViewModel负责管理基于用户交互的UI状态的改变。

在ViewModel层创建SignupObservableVM,并定义如下序列来表示各种UI状态:

class SignupObservableVM {
    // 用户名有效验证的序列
    let validatedUsername: Observable<ValidationResult>
    // 密码有效验证的序列
    let validatedPassword: Observable<ValidationResult>
    // 重复密码有效验证的序列
    let validatedRepeatedPassword: Observable<ValidationResult>
    // 允许注册的序列
    let signupEnabled: Observable<Bool>
    // 注册的序列
    let signedIn: Observable<Bool>
    // 注册中的序列
    let signingIn: Observable<Bool>

初始化ViewModel,接收View层的事件:

    /// 初始化
    /// - Parameters:
    ///   - input: 输入序列元组
    ///   - dependency: 依赖的功能模型
    init(
        input: (
            username: Observable<String>,// 用户名输入序列
            password: Observable<String>,// 密码输入序列
            repeatedPassword: Observable<String>,// 二次密码输入序列
            signTaps: Observable<Void>),// 注册点击序列
        dependency: (
            API: GithubApi,
            service: GitHubValidationService,
            wireframe: WireFrame))

有效验证状态

在初始化函数中实现表示用户名、密码、二次输入密码是否有效的序列:

        validatedUsername = input.username
            .flatMapLatest({ (name) in
            return service.validateUsername(name)
                .observeOn(MainScheduler.instance)
                .catchErrorJustReturn(.failed(message: "服务器报错"))
            }).share(replay: 1)
        
        validatedPassword = input.password
            .map({ (password) in
                return service.validatePassword(password)
            }).share(replay: 1)
        
        validatedRepeatedPassword = Observable
            .combineLatest(input.password, input.repeatedPassword, resultSelector: service.validateRepeatedPassword)
            .share(replay: 1)

说明:使用map操作符做元素的转换,由于validatedUsername返回的一个序列所以使用flatMapLatest操作符来展平及忽略旧的序列,用observeOn操作符来保证线程,用catchErrorJustReturn操作符保证不会发生错误。最后使用share操作符来共享序列元素。

注册状态

创建一个ActivityIndicator序列实例用于表示是否正在注册中:

        let signingIn = ActivityIndicator()
        self.signingIn = signingIn.asObservable()

组合用户名和密码的输入序列:

        let up = Observable.combineLatest(input.username, input.password) { (username: $0, password: $1) }

实现表示是否允许注册的signupEnabled序列:

        signupEnabled = Observable
            .combineLatest(
                validatedUsername,
                validatedPassword,
                validatedRepeatedPassword,
                self.signingIn,
                resultSelector: { (un, pd, repd, sign) in
                    un.isValidate && pd.isValidate && repd.isValidate && !sign
                }
            ).distinctUntilChanged()
            .share(replay: 1)

代码分析:

  1. 使用combineLatest操作符合并用户名有效验证、密码有效验证、二次输入密码有效验证、是否正在注册中的序列,组合不在登录中,其他全部有效的BOOL值返回
  2. 使用distinctUntilChanged操作符保证序列值发生变化时发出元素
  3. 最后使用share操作符达到共享序列元素的效果

实现表示是否注册成功的signedIn序列:

        signedIn = input.signTaps
            .withLatestFrom(up)
            .flatMapLatest({ (pair) in
                return api.signup(pair.username, password: pair.password)
                    .observeOn(MainScheduler.instance)
                    .catchErrorJustReturn(false)
                    .trackActivity(signingIn)
            }).flatMapLatest({ (loggedIn) -> Observable<Bool> in
                let message = loggedIn ? "GitHub注册成功" : "GitHub注册失败"
                return DefaultWireFrame()
                    .promptFor("提示", message: message, cancelAction: "确定", actions: ["否"])
                    .map({ _ in loggedIn })
            }).share(replay: 1)

代码分析:

  1. 注册点击序列使用withLatestFrom操作符将元素转化为用户名与密码的输入组合序列的最新元素
  2. 使用flatMapLatest操作符返回注册序列
  3. 注册序列使用observeOn、catchErrorJustReturn操作符保证注册序列在主线程执行并不会出错
  4. 注册序列执行trackActivity操作,记录序列的状态
  5. 再使用flatMapLatest操作符返回消息提示序列
  6. 最后使用share操作符达到共享序列元素的效果

绑定UI

定义一个ValidationColors结构体,使用3个类属性来表示个状态的颜色。:

/// 有效的颜色
struct ValidationColors {
    static let defaultColor = UIColor.black // 默认黑色
    static let okColor = UIColor(red: 138.0 / 255.0, green: 221.0 / 255.0, blue: 109.0 / 255.0, alpha: 1.0) // 有效为绿色
    static let errorColor = UIColor.red // 失败为红色
}

扩展ValidationResult,定义isValidate、description、textColor3个计算属性:

/// 有效结果扩展
extension ValidationResult {
    /// 是否有效
    var isValidate: Bool {
        switch self {
        case .ok:
            return true
        default:
            return false
        }
    }
    
    /// 描述
    var description: String {
        switch self {
        case let .ok(message):
            return message
        case .empty:
            return ""
        case .validating:
            return "加载中..."
        case let .failed(message):
            return message
        }
    }
    
    /// 文本颜色
    var textColor: UIColor {
        switch self {
        case .ok:
            return ValidationColors.okColor
        case .empty:
            return ValidationColors.defaultColor
        case .validating:
            return ValidationColors.defaultColor
        case .failed:
            return ValidationColors.errorColor
        }
    }
}

扩展泛型Base为UILabel类型的Reactive,增加一个Binder<ValidationResult>类型的validationResult属性将ValidationResult绑定到UILabel的textColor、text两个属性上:

extension Reactive where Base: UILabel {
    var validationResult: Binder<ValidationResult> {
        return Binder(base, binding: { (label, result) in
            label.textColor = result.textColor
            label.text = result.description
        })
    }
}

将ViewModel中各种表示UI状态的序列绑定到对应的UI控件上:

        // 绑定UI
        vm.validatedUsername
            .bind(to: usernameValidationOutlet.rx.validationResult)
            .disposed(by: bag)
        vm.validatedPassword
            .bind(to: passwordValidationOutlet.rx.validationResult)
            .disposed(by: bag)
        vm.validatedRepeatedPassword
            .bind(to: repeatValidationOutlet.rx.validationResult)
            .disposed(by: bag)
        
        vm.signupEnabled
            .bind(to: signupOutlet.rx.isEnabled)
            .disposed(by: bag)
        vm.signupEnabled
            .map { $0 ? 1.0 : 0.5 }
            .bind(to: signupOutlet.rx.alpha)
            .disposed(by: bag)
        vm.signingIn
            .bind(to: signingupOutlet.rx.isAnimating)
            .disposed(by: bag)
        vm.signingIn
            .subscribe(onNext: { [weak self] (signing) in
                if (signing) { self?.view.endEditing(signing) }
            }).disposed(by: bag)
        vm.signedIn
            .subscribe(onNext: { (signed) in
                print("用户注册\(signed ? "成功" : "失败")")
            }).disposed(by: bag)
        
        let tap = UITapGestureRecognizer()
        tap.rx.event
            .subscribe(onNext: { [weak self] (tap) in
                self?.view.endEditing(true)
            }).disposed(by: bag)
        view.addGestureRecognizer(tap)

使用Driver的实现方案

使用Driver的实现方式与Observe完全是一样的,基于Driver以下特点:

在创建表示UI状态的序列时,使用Driver或者用asDriver操作符转化为Driver,就可以满足observeOncatchErrorJustReturnshare这三个操作符的效果,简化编码。

Driver序列的绑定是使用drive操作符。

总结

上一篇下一篇

猜你喜欢

热点阅读