RxSwift+MVVM 常用数据流处理

在RxSwift应用MVVM中提到了, Rx框架在MVVM设计模式中, 担任数据流处理的信号出口. 以下分享一下这个数据出口在日常开发中不同场景中分别做了什么处理.

网络请求

  • Data 转换JSON
    开发中使用了Moya/RxSwift框架, 在网络请求成功的情况下, 数据是以二进制data形式封装在Moya.Response中的, 而日常开发中, 这个data基本都是json格式的数据, 站在开发效率考虑, 我们不希望每次请求完都要调用者把data转换成json, 所以可以配合SwiftyJSON框架, 在WebAPI层面提前把response数据转换成JSON对象.
  • 聚合Network ErrorBusiness Error
    我们面对的错误分为两种: 网络错误业务错误.
    网络请求的API会帮我们处理网络错误, 比如请求超时, 网络断开, 找不到地址这类. 而业务错误错误是在网络请求成功的情况下才会出现的.
    按照开发习惯, 每一个异步网络请求函数都有2个回调的入口(一般以闭包的形式): successfailure.但是如果需要每次调用的时候再在success闭包中做业务错误判断, 会让代码比较难看, 而且也不符合successfailure闭包分工的初衷, 利用Rx框架我们同样可以在WebAPI层面提前把Resonse中的的ErrorCode做处理, 做成Error抛出. 这样就可以达到让调用者在failure中统一处理Error的目的.
  • Error 映射
    网络错误返回的信息都是技术描述, 如果把这些错误信息直接展示给用户会非常不友好, 我们可以使用Rx的catchError函数把网络错误信息提前转换成方便提示用户的数据.
extension MoyaProvider {
    public func customRequest(_ target: Target) -> Observable<(data: JSON, headers: JSON)> {
        return
            self.rx.request(target)
                .asObservable()
                .timeout(ApiConfig.timeoutInterval, scheduler: MainScheduler.asyncInstance)
                .catchError{ (error) -> Observable in
                    /*
                     网络错误, 网络请求不成功, 没有响应, 把网络错误文案(英文)转换成用户可阅读的文案(中文)
                     */
                    switch error {
                    case is MoyaError: // Moya框架封装的网络错误
                        throw mapMoyaError(error as! MoyaError) ?? error
                    case is RxError: // 可能为超时错误
                        throw mapRxError(error as! RxError) ?? error
                    default: // 不可能触发
                        throw error
                    }
                }
                .map({ response -> Moya.Response in
                    
                    let headers = JSON(response.response?.allHeaderFields ?? [:])
                    
                    switch response.statusCode {
                    case let x where x >= 200 && x < 300:
                        // 请求成功, 没有网络状态错误, 进入下一步
                        return response
                    default:
                        // http错误(网络请求成功, 报错)
                        // 服务端协商好的错误提示
                        if let error = mapCustomError(statusCode: response.statusCode)
                        {
                            throw error
                        }
                        // 非协商好的错误提示
                        let json = try? JSON(data: response.data)
                        let rawString = json?.rawString()
                        let utf8String = String(data: response.data, encoding: .utf8)
                        let domain = (rawString ?? utf8String) ?? response.debugDescription
                        throw NSError( domain: domain, code: response.statusCode, userInfo: nil)
                    }
                    
                })
                .map { response -> JSON in
                    
                    guard
                        let json = try? JSON(data: response.data)
                        else
                    {
                       throw NSError( domain: "数据解析失败", code: -1, userInfo: nil)
                    }
                    
                    // 服务端业务代码, 0为API调用成功
                    let code = json["code"].intValue
                    guard
                        code == 0
                        else
                    {
                        // 接口请求成功, 但是报错
                        let domain = json["msg"].string ?? "服务端产生未知错误"
                        let customError = mapCustomError(statusCode: code)
                        let defaultError = NSError(domain: domain, code: code, userInfo: nil)
                        throw customError ?? defaultError
                    }
                    // 没有业务错误, 成功
                    return json["data"]
                }
    }
}

数据流中心 Service

  • 数据变更广播
    Service模块中有不少异步处理数据的场景, 比如:

    1. 网络请求
    2. 定位变化
    3. 权限变化
    4. 应用状态变化
      ...

    以上事件发生后Service模块会做相应处理, 并更新自身持有的数据. 这些变更数据有一部分需要实时被其他模块实时获取, 这种时候我们可以使用Rx框架里面的SubjectType类型:

    Represents an object that is both an observable sequence as well as an observer.

    SubjectType的特性: 持有者可以用它来发送信号, 它本身也可以被多次订阅.

    在Rx框架中, 默认有4种SubjectType

    1. AsyncSubject
    2. BehaviorSubject
    3. PublishSubject
    4. ReplaySubject

    在Service模块中, 重点使用到的一般是PublishSubjectReplaySubject, 这里首先说PublishSubject.
    比方 用户点击推送 这件事, 会触发很多APP的响应, 而具体响应需要从通知的userInfo中获取, 事实上我们可能也只需要处理其中一部分.
    Service在这个事件链里面, 需要获取用户点击推送的回调, 并且把userInfo做解析, 然后把解析好的数据广播出去.

    class NotificationService: NSObject {
      
      static let shared: NotificationService = NotificationService()
      
      private override init(){}
      ...
      ...
      ...
      // 广播信号
      fileprivate let clickPayload: PublishSubject = PublishSubject()
    }
    
    extension NotificationService {
      // 用户点击推送的参数
      typealias ClickRemoteNotificationParams = (content: JSON, pushId: String?)
    }
    
    extension NotificationService : UNUserNotificationCenterDelegate  {
      
      @available(iOS 10.0, *)
      func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
    
          let remoteJson = JSON(response.notification.request.content.userInfo)
    
          let customContent = JSON(parseJSON: remoteJson["pushContent"].stringValue)
          // 发送广播信号
          clickPayload.onNext((customContent, customContent["pushId"].string))
          
          completionHandler()
      }
    }
    

    到这里, 广播信号发送了, 但是我们在这里把广播信号clickPayload声明为了fileprivate, 这样的话我们只能在文件内部访问它, 外部是不知道这个变量的.

    为什么?

    因为,SubjectType的特性之一就是, 持有者可以用它发信号. 然而在理想设计中, 我们不希望在Service模块以外可以直接控制Service自身的数据流, 所以clickPayload不可以被外部直接访问. 而且, 我们希望Rx信号对于外部来说是黑盒化的, 参考Rx框架的做法, 我们可以做以下处理:

     extension Reactive where Base: NotificationService {
      var clickPayload: Observable {
          return base.clickPayload.share()
      }
     }
    

    通过share函数, 把原来可以充当数据入口和数据出口PublishSubject转换成只充当数据出口的Observable.

    Returns an observable sequence that shares a single subscription to the underlying sequence, and immediately upon subscription replays elements in buffer.

    这样, 在NotificationService外部, 就可以用以下形式去订阅用户点击推送的广播

    NotificationService.shared
              .rx.clickPayload
              .subscribe(onNext: { (value) in
                  // value处理
              })
    
  • 预加载数据
    为了提升用户体验, 我们会在用户打开一些页面之前就加载/请求好数据.
    我们在应用启动/用户浏览其他简单页面的同时, 在后台开始请求/加载, 等用户打开页面后, ViewController就可以加载已经处理好的函数, 如果这时候仍未处理完预加载的数据, 页面也可以拿到之前已经在执行的请求任务, 这样也可以大大缩短用户的等待时间.
    归纳一下, 我们需要做到以下几点:

    1. 数据加载的操作和页面的生命周期无关
    2. 数据加载完毕后可以传递给页面
    3. 数据加载未完成的时候, 页面打开, 加载任务可以继续执行, 完毕后马上传递给页面
    4. 如果加载失败, 需要反映到页面上, 或者在页面打开的时候再一次尝试加载数据

    Service模块在设计上分离了ViewController, 第1点天然达成了. 我们很容易想到, 这种情况我们还是利用上一步的方案, 提前调用Service的函数, 在对应的ViewController中订阅这个任务的Observable. 而当我们使用了上一个方案的时候, 会发现:

    我们打开页面的时候, 总是没有数据.

    这是因为PublishSubject的信号时效性引起的.

    http://reactivex.io/documentation/subject.html
    Note that a PublishSubject may begin emitting items immediately upon creation (unless you have taken steps to prevent this), and so there is a risk that one or more items may be lost between the time the Subject is created and the observer subscribes to it. If you need to guarantee delivery of all items from the source Observable, you’ll need either to form that Observable with Create so that you can manually reintroduce “cold” Observable behavior (checking to see that all observers have subscribed before beginning to emit items), or switch to using a ReplaySubject instead.

    RxSwift+MVVM 常用数据流处理_第1张图片
    PublishSubject信号发送和订阅时间的关系

    所以实际上, 我们在数据加载完毕之后再订阅, 是没办法拿到数据的.

    Rx给我们提供了ReplaySubject:

    Each notification is broadcasted to all subscribed and future observers, subject to buffer trimming policies.


    RxSwift+MVVM 常用数据流处理_第2张图片
    ReplaySubject信号发送和订阅时间的关系

    使用ReplaySubject有一个关键的参数bufferSize, 它决定了我们希望获取订阅前发送的多少条数据. 一般预加载, 只需要获取1条.

    class EventService {
    
      static let shared: EventService = EventService()
      
      private init() {}
    
      ...
      ...
      ...
    
      fileprivate(set) var eventList: [Event] = []
    
      fileprivate let _eventList: ReplaySubject<[Event]> = ReplaySubject.create(bufferSize: 1)
    }
    
    extension EventService {
      // 处理reload请求的json数据
      func handleEventListDidReload(json: JSON) -> [Event] {
          // 更新eventList
          
          ...
          ...
    
          _eventList.onNext(eventList)
    
          return eventList
      }
    }
    
    extension Reactive where Base: EventService {
    
      var eventListDidUpdate: Observable<[Event]> {
          return base._eventList.share()
      }
    
      func reloadEventList() -> Observable<[Event]> {
          return
           kEventApiProvider
            .customRequest(.eventList)
            .map(base.handleEventListDidReload)
      }
    }
    

    AppDelegate的启动回调中提前调用函数:

    class AppDelegate: UIResponder, UIApplicationDelegate {
    
      var window: UIWindow?
      
      /*
       life cycle
       */
    
      func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
          ...
          ...
          ...
    
          _ = EventService.shared.rx.reloadEventList().subscribe()
    
          return true
      }
    }
    

    ViewControllerviewDidLoad中订阅数据更新:

    override func viewDidLoad() {
      super.viewDidLoad()
    
      ...
      ...
      ...
    
      EventService.shared
        .rx.eventList
        .subscrib(onNext: { eventList in
          // 响应数据更新
        },
        onError: { error in 
          // 处理Error
        })
      }
    

PS: 实际上在我们只需要获取1条最近的广播的情况下, 我们还可以使用另一个Rx框架提供的类型Variable. 只是由于在理想情况下我们不希望外部模块可以使用Variable发送广播, 所以我们还是会让外部用Observable的形式访问广播信号, 而对应地, 实时数据也会映射Variable. 如此一来, 使用可以控制缓冲大小的ReplaySubject就相对更灵活了.

ViewModel

  • 映射/处理Service的Observable
    在MVVM模式中, Model层和View层是不会直接进行通信, 也就是说, 其实上一段代码中, ViewController中直接订阅EventService的数据更新广播, 是不应该发生的, 那是MVC模式里面的做法.
    取而代之, 我们会在ViewModel的模块中订阅数据更新的广播, 并在ViewModel模块中完成Model变化的业务处理, 转换成View层可以直接无脑加载的数据, 再输出:
class ViewControllerViewModel {

  var eventList: Observable<[EventViewModel]> {
    return 
      EventService.rx.eventList
        .map { $0.map { EventViewModel(event: $0) } } // 把Model映射成ViewModel
        .subscribeOn(scheduler) // 确保映射操作在并发队列中进行
        .observeOn(MainScheduler.asyncInstance) // 数据映射完毕后切回主队列
  }

  // Rx框架提供的用于切换队列的类型
  private let scheduler = ConcurrentDispatchQueueScheduler(qos: .default)
  
  func reloadEventList() {
    // 调用Service的函数, 等待副作用 (由于篇幅此处暂不列出Error的处理方式)
    EventService.rx.reloadEventList().subscribe()
  }
}

PS:
1.以上代码只简单举例一种数据广播映射的情况, 实际开发中经常会遇到多个数据更新而引发一个界面的刷新, 思路同样是使用映射, 只是需要用到Rx框架的其他Operator函数.
2.以上代码没有列出请求发生Error的情况, 实际上Error的处理根据需求不同有不同的实现方式.
上述两点在RxSwift, 异步操作组合处理中均有提及.

View/ViewController

在MVVM模式中, View层面向ViewModel层, 它专注做3件事.

  1. 绑定ViewModel事件
  2. UI组件操作回调
  3. 实现UI更新逻辑

其中Rx框架主要参与前2件事中:

  class ViewController: UIViewController {

    let viewModel: ViewModel = ViewModel()

    var disposeBag = Disposebag()

    let reloadButton = UIButton()
    ...
    ...
    ...

    override func viewDidLoad() {
      super.viewDidLoad()
      ...
      ...
      ...
      // 响应ViewModel的广播
      viewModel.rx.eventList.subscribe(onNext: handleEventListUpdate)
      // 响应用户的操作, 调用ViewModel的刷新函数
      reloadButton.rx.tap.subscribe(onNext: viewModel.reloadEventList)
    }

    func handleEventListUpdate(eventList: [EventViewModel]) {
      // 更新UI
      ...
      ...
      ...
    }
  }

你可能感兴趣的:(RxSwift+MVVM 常用数据流处理)