美文网首页RxSwiftiOS Developer
RxSwift_v1.0笔记——17 Creating Cust

RxSwift_v1.0笔记——17 Creating Cust

作者: 大灰很 | 来源:发表于2017-09-30 17:19 被阅读82次

    介绍RxSwift, RxCocoa,之后,我们学习了如何测试,你也看到了通过Apple或第三方使用RxSwift在框架顶层如何创建扩展。在关于RxCocoa的章节介绍了封装一个Apple或第三方框架的组件,因此通过这个章节的项目用你工作的方式来扩展你所学到的。

    在这章,你将给NSURLSession创建一个扩展来管理同端点的通讯,也管理了缓存和其他东西,它是常规应用的普通的部分。这个例子是为教学用的;如果你想使用RxSwift到网络上,有一些库可以给你来使用,包含RxAlamofire,本书也会覆盖这方面的知识。

    开始 315

    首先你要在https://developers.giphy.com/ 注册并申请API key

    打开ApiController.swift,复制你的key到下面位置:

    private let apiKey = "[YOUR KEY]"
    

    然后用pod install命令安装第三方库。

    怎样创建扩展 315

    在Cocoa类或框架之上创建扩展可能看起来像是不平凡的任务;您将看到该过程可能很棘手,您的解决方案可能需要一些前期的思考才能继续。

    这节的目标是用rx命名空间扩展URLSession,孤立的RxSwift扩展,确保了你或你的团队在将来需要扩展这个类时,几乎也不会产生冲突。

    如何用.rx来扩展URLSession 315

    打开URLSession+Rx.swift,增加下面代码

    extension Reactive where Base: URLSession {
    }
    

    响应式扩展通过非常清晰的协议扩展,在URLSession上暴露.rx命名空间。这是用RxSwift扩展URLSession的第一步。现在是时候创建实际的封装了。

    如何创建封装的方法 315

    您已经在NSURLSession上暴露了.rx命名空间,因此现在可以创建一些封装的函数来返回要公开的数据类型的Observable。

    APIs能够返回多种类型的数据,正确的做法是检查你app需要的数据类型。你希望为下列类型数据创建封装:

    • Data:仅仅是数据
    • String:数据作为文本
    • JSON:JSON对象的一个实例
    • Image:图像的一个实例

    这些封装将确保你期望的类型被投递。否则将发送错误,且app将输出错误而不会崩溃。

    这个,和一个将被用来创建所有其他的东西的封装,是一个返回HTTPURLResponse和结果数据的封装。你的目标是给一个 Observable<Data>,它将被用来创建剩下的三个操作:

    首先创建主要响应函数的框架,这样你就知道要返回的内容了。在你刚刚创建的扩展内部增加:

    func response(request: URLRequest) -> Observable<(HTTPURLResponse, Data)>
    {
      return Observable.create { observer in
        // content goes here
        return Disposables.create()
      }
    }
    

    现在很清楚扩展应该返回什么了。URLResponse是需要你检查的部分,当数据到达时,用来确保处理成功,当然,实际的数据用它返回。

    URLSession是基于回调和任务的。例如内建方法 dataTask(with:completionHandler:) 会发送一个请求并接收来至服务器的响应。这个函数使用回调来管理结果,因此你的observable的逻辑必须在请求闭包内部被管理。

    为了实现以上内容,在Observable.create内部增加:

    let task = self.base.dataTask(with: request) { (data, response, error) in
    }
    task.resume()
    

    创建的任务必须被恢复(或启动),因此resume()函数将发出请求,然后通过回调来适当地处理结果。

    Note:使用resume()函数是所谓的“命令式编程”。 稍后你会看到这些意味着什么。
    

    现在这个任务已经就位了,在继续之前需要做一个改变。 在上一个块中,您返回了一个Disposable.create(),如果Observable被销毁,这将什么都不做。 最好取消请求,以免浪费任何资源。

    为了实现以上内容,用以下内容替换return Disposables.create():

    return Disposables.create(with: task.cancel)
    

    现在,您已经拥有了具有正确生命周期策略的Observable,现在是时候确保在给这个实例发送任何事件前,数据是正确的了。 要实现这一点,请将以下内容添加到task.resume()上方的task闭包中:

    guard let response = response, let data = data else {
      observer.on(.error(error ?? RxURLSessionError.unknown))
      return
    }
    guard let httpResponse = response as? HTTPURLResponse else {
      observer.on(.error(RxURLSessionError.invalidResponse(response:
        response)))
      return
    }
    

    两个guard申明在通知所有订阅前,确保了请求已经成功执行。

    保证请求正确完成后,这个observable需要一些数据。在你刚增加的代码下面增加以下代码:

    observer.on(.next(httpResponse, data))
    observer.on(.completed)
    

    这将事件发送到所有订阅,然后立即完成。 触发请求并接收其响应是单次Observable的用法。 保持可观察的活动并执行其他请求是没有意义的,这更适合于socket通信等。

    这是封装URLSession的最基本的操作。 您将需要包装更多的东西,以确保应用程序正在处理正确的数据类型。 好消息是,您可以重用此方法来构建其他便利方法。 首先添加一个返回Data实例的:

    func data(request: URLRequest) -> Observable<Data> {
      return response(request: request).map { (response, data) -> Data in
        if 200 ..< 300 ~= response.statusCode {
          return data
        } else {
          throw RxURLSessionError.requestFailed(response: response, data:
            data)
        }
      }
    }
    

    Data observable是所有其他的根基。Data能够装换为String,JSON对象或UIImage。

    增加下面方法来返回String:

    func string(request: URLRequest) -> Observable<String> {
      return data(request: request).map { d in
        return String(data: d, encoding: .utf8) ?? ""
      }
    }
    

    JSON数据结构是一个简单的结构,所以专用的转换是受欢迎的。 增加:

    func json(request: URLRequest) -> Observable<JSON> {
      return data(request: request).map { d in
        return JSON(data: d)
      }
    }
    

    最后,实现最后一个用来返回UIImage实例的方法:

    func image(request: URLRequest) -> Observable<UIImage> {
      return data(request: request).map { d in
        return UIImage(data: d) ?? UIImage()
      }
    }
    

    当您像您刚才那样模块化扩展时,您可以实现更好的组合性。 例如,最后一个可观察值可以通过以下方式可视化:

    一些RxSwift的操作,如map,可以智能的组合,以避免处理开销,因为多个map链将被优化为单个调用。 不要担心链接或包含太多的闭包。

    如何创建自定义运算 319

    在关于RxCocoa的章节里,你创建了一个缓存数据的函数。考虑到GIFs的尺寸,这看起来像是一个好的方法。同样,一个好的应用应该尽可能的减少加载时间。

    在这个情况下,好的方法是创建一个专用的操作来缓存数据,它仅仅用于 (HTTPURLResponse, Data)类型的observables。这个的目的是尽可能多的缓存,因此创建这个操作仅仅为(HTTPURLResponse, Data)类型是合理的,并且使用这个响应对象来检索请求绝对的URL,然后将它作为字典的key来使用。

    缓存策略是一个简单的字典;你能够稍后扩展它的基本行为来固化缓存,并当重新打开app时加载它,但是这超出了当前项目的范围。

    在顶部, RxURLSessionError的定义之前,创建缓存字典:

    fileprivate var internalCache = [String: Data]()
    

    然后创建扩展,它的目标仅为Data类型的observables

    extension ObservableType where E == (HTTPURLResponse, Data) {
    }
    

    在这个扩展内部,你可以创建以下 cache() 函数:

    func cache() -> Observable<E> {
      return self.do(onNext: { (response, data) in
        if let url = response.url?.absoluteString, 200 ..< 300 ~=
          response.statusCode {
          internalCache[url] = data
        }
      })
    }
    

    为了使用这个缓存,确保在返回它拥有的结果前像下面这个样(你可以简单的插入.cache()部分),来修改 data(request:)的返回状态来缓存响应:

    return response(request: request).cache().map { (response, data) -> Data
    in
    //...
    }
    

    为了检测数据是否已经有效,增加下面代码到 data(request:)顶部,return前,来替代每次启动一个网络请求:

    if let url = request.url?.absoluteString, let data = internalCache[url] {
      return Observable.just(data)
    }
    

    现在你有了一个基本的缓存系统,它仅仅扩展了一个确定类型的Observable:

    你可以重复同样的步骤来缓存其他类型的数据,这是一个极其普遍的解决方案。

    使用自定义封装 320

    你已经创建了一些关于URLSession的封装,也对一些特定类型的observables自定义了操作目标。是时候抓取一些结果来显示一些有趣的猫的GIFs了。

    当前项目已经包含了batteries,因此你仅仅需要提供来自Giphy的API提供的JSON结构的列表。

    打开ApiController.swift并查看 search()方法。代码内部准备了一个适当的请求到Giphy的API,但在最底部,它没有做网络调用,而是仅仅返回一个空的observable(因为这是一个占位代码)。

    现在你已经完成了你的URLSession响应式扩展,在这个定制的方法中,你能够用它来从网络获取数据。像下面这样修个返回状态:

    return URLSession.shared.rx.json(request: request).map() { json in
      return json["data"].array ?? []
    }
    

    这将为给定的查询字符串处理请求,但是数据任然没有显示。在GIF实际上显示屏幕之前,最后一步要执行。

    增加下面代码到GifTableViewCell.swift中 downloadAndDisplay(gif stringUrl:):的末尾

    let s = URLSession.shared.rx.data(request: request)
      .observeOn(MainScheduler.instance)
      .subscribe(onNext: { imageData in
        self.gifImageView.animate(withGIFData: imageData)
        self.activityIndicator.stopAnimating()
      })
    disposable.setDisposable(s)
    

    SingleAssignmentDisposable()的使用是强制性的,以保持效果良好。 当GIF的下载开始时,如果用户滚动并且不等待渲染图像,则应确保它已停止。 为了正确平衡这一点,在prepareForReuse())中有这两行(已经包含在起始代码中,现在不需要键入它们):

    disposable.dispose()
    disposable = SingleAssignmentDisposable()
    

    SingleAssignmentDisposable()将确保每个单个单元格在给定时间只有一个订阅活动,所以您不会浪费资源。

    构建并运行,在搜索栏中输入内容,您将看到应用程序活着。

    测试自定义封装 321

    虽然它看起来一切正常,但请创建一些测试来确保当你将来开发代码时一切都保持工作正常。这是一个好习惯,尤其是当你封装第三方框架时。

    测试用例确保框架围绕的封装保持良好的形状,并且将帮助您找到代码由于更改或错误而发生故障的位置。

    如何为自定义封装写测试 322

    在上一章中介绍了测试; 在本章中,您将使用用于在Swift上编写测试的通用库,称为Nimble,以及其封装RxNimble。

    RxNimble使测试更易于编写,并使你的代码更简洁。代替普通的写法:

    let result = try! observabe.toBlocking().first()
    expect(result) != 0
    

    你可以写的更短:

    expect(observable) != 0
    

    打开测试文件iGifTests.swift。查看import部分,你可以看到Nimble,RxNimble,OHHTTPStubs用于存储网络请求,RxBlocking将异步操作转换为阻塞请求。

    在文件末尾你也能够找到用单一的函数来为 BlockingObservable进行的扩展

    func firstOrNil() -> E? {}
    

    这样做可以避免滥用try? 方法全部通过测试文件。 你会很快看到这个的使用。

    在文件顶部,你将找到一个伪造的JSON对象来测试:

    let obj = ["array": ["foo", "bar"], "foo": "bar"] as [String: Any]
    

    使用这个预定义的数据让你更容易为Data,String和JSON请求写测试。

    第一个要写的测试时为data请求。增加下列测试到test实例类来检查请求不是返回nil:

    func testData() {
      let observable = URLSession.shared.rx.data(request: self.request)
      //原报错:use beNil() to match nils
      //expect(observable.toBlocking().firstOrNil()) != nil
      expect(observable.toBlocking().firstOrNil()).notTo(beNil())
    }
    

    在这个方法中,一旦你完成(wrap up)输入,Xcode将在编辑器槽中显示一个菱形按钮,很像这样(行号可能会有所不同):

    点击这个按钮并运行测试。如果测试成功,按钮将变绿;如果它失败,它将变红。如果你顺利的输入所有正确的代码,你将看到按钮变为绿色的检查标志。

    一旦observable返回被测的数据并工作正确,下一步是测试observable来处理字符串。考虑到原始数据是JSON形式,并且keys被分类了,期望的结果应该是:

    {"array":["foo","bar"],"foo":"bar"}
    

    接下来的测试写起来真的很简单。添加以下内容,考虑到必须转义JSON字符串:

    func testString() {
      let observable = URLSession.shared.rx.string(request: self.request)
      let string = "{\"array\":[\"foo\",\"bar\"],\"foo\":\"bar\"}"
      expect(observable.toBlocking().firstOrNil()) == string
    }
    

    点击测试按钮来测试新的测试用例,一旦完成,继续测试JSON解析。这个测试需要一个JSON数据结构来进行比较。增加下列代码来转换字符串版本到Data并处理它为JSON::

    func testJSON() {
      let observable = URLSession.shared.rx.json(request: self.request)
      let string = "{\"array\":[\"foo\",\"bar\"],\"foo\":\"bar\"}"
      let json = JSON(data: string.data(using: .utf8)!)
      expect(observable.toBlocking().firstOrNil()) == json
    }
    

    最后一个测试是确保错误返回正确。 比较两个错误是一个相当不寻常的过程,因此对于错误而言具有相等的运算符是没有意义的。 因此测试应该使用do,try并catch未知错误。

    增加下列代码:

    func testError() {
      var erroredCorrectly = false
      let observable = URLSession.shared.rx.json(request: self.errorRequest)
      do {
        let _ = try observable.toBlocking().first()
        assertionFailure()
      } catch (RxURLSessionError.unknown) {
        erroredCorrectly = true
      } catch {
        assertionFailure()
      }
      expect(erroredCorrectly) == true
    }
    

    此时您的项目已完成。 您已经在URLSession之上创建了自己的扩展,并且还创建了一些很酷的测试,这将确保您的封装的行为正确。 对你所建立的封装进行测试是非常重要的,因为Apple框架和其他第三方框架可以在主要版本中变化 - 所以如果测试中断并且封装停止工作,您应该准备快速行动。

    常见的有效封装 324

    RxSwift社区非常活跃,有许多扩展和封装已经可用。一些事基于Apple的组件,一些是基于在许多iOS和macOS项目上使用广泛的第三方库。

    你可以在下面网站找的最新的(up-to-date)封装列表:http://community.rxswift.org

    RxDataSources 324

    RxDataSources是一个用于RxSwift的UITableView和UICollectionView数据源,具有一些非常好的功能,如:

    • 用于计算差异的O(N)算法
    • 启发式发送最少数量的命令到sectioned视图
    • 支持扩展已实施的视图
    • 支持层次动画

    这些都是重要的功能,但我最喜欢的是用于区分两个数据源的O(N)算法 - 它确保了在管理表视图时应用程序不执行不必要的计算。
    考虑使用内置的RxCocoa表绑定编写的代码:

    let data = Observable<[String]>.just(
      ["1st place", "2nd place", "3rd place"]
    )
    data.bindTo(tableView.rx.items(cellIdentifier: "Cell")) { index, model,
      cell in
      cell.placeLabel.text = model
    }
    .addDisposableTo(disposeBag)
    

    这个用简单的数据设置完美工作,但是缺少动画和对多个sections的支持,并且不能很好地扩展。

    通过RxDataSource正确配置,代码变得更加健壮:

    //configure sectioned data source
    let dataSource =
      RxTableViewSectionedReloadDataSource<SectionModel<String, String>>()
    //bind data to the table view, using the data source
    Observable.just(
      [SectionModel(model: "Position", items: ["1st", "2nd", "3rd"])]
    )
    .bindTo(tableView.rx.items(dataSource: dataSource))
    .addDisposableTo(disposeBag)
    

    并且需要预先完成的数据源的最小配置如下所示:

    dataSource.configureCell = { dataSource, tableView, indexPath, item in
      let cell = tableView.dequeueReusableCell(
        withIdentifier: "Cell", for: indexPath)
      cell.placeLabel.text = item
      return cell
    }
    dataSource.titleForHeaderInSection = { dataSource, index in
      return dataSource.sectionModels[index].header
    }
    

    由于绑定table和collection视图是重要的每日任务,您将在本书后面的专用章节cookbook-style中更详细地查看RxDataSources。

    RxAlamofire 325

    RxAlamofire是优雅的Swift HTTP网络库Alamofire的封装。 Alamofire是最受欢迎的第三方框架之一。
    RxAlamofire具有以下便利扩展功能:

    func data(_ method:_ url:parameters:encoding:headers:)
      -> Observable<Data>
    

    此方法将所有请求详细信息合并到一个调用中,并将服务器响应作为Observable <Data>返回。

    而且,这个库还提供了:

    func string(_ method:_ url:parameters:encoding:headers:)
      -> Observable<String>
    

    它返回一个String类型的Observable的内容响应

    最后,但同样重要:

    func json(_ method:_ url:parameters:encoding:headers:)
      -> Observable<Any>
    

    它返回一个对象的实例。 重要的是要知道,此方法不会返回像之前创建的JSON对象

    RxBluetoothKit 326

    使用蓝牙可能很复杂。 一些调用是异步的,调用的顺序对于从设备或外围设备正确连接,发送数据和接收数据至关重要。

    RxBluetoothKit抽象了一些使用蓝牙的最痛苦的部分,并提供了一些很酷的功能:

    • CBCentralManger 支持
    • CBPeripheral 支持
    • 扫描共享和排队

    开始使用RxBluetoothKit,你必须创建一个manager:

    let manager = BluetoothManager(queue: .main)
    

    扫描外设的代码看起来像:

    manager.scanForPeripherals(withServices: [serviceIds])
      .flatMap { scannedPeripheral in
      let advertisement = scannedPeripheral.advertisement
    }
    

    并连接到一个:

    manager.scanForPeripherals(withServices: [serviceId])
      .take(1)
      .flatMap { $0.peripheral.connect() }
      .subscribe(onNext: { peripheral in
      print("Connected to: \(peripheral)")
      })
    

    也可以观察当前manager的现状:

    manager.rx_state
      .filter { $0 == .poweredOn }
      .timeout(1.0, scheduler)
      .take(1)
      .flatMap { manager.scanForPeripherals(withServices: [serviceId]) }
    

    除了manager外,还有特色和外设的超级方便抽象。 例如,要连接外设,您可以执行以下操作:

    peripheral.connect()
      .flatMap { $0.discoverServices([serviceId]) }
      .subscribe(onNext: { service in
      print("Service discovered: \(service)")
      })
    

    如果你想发现一个特征:

    peripheral.connect()
      .flatMap { $0.discoverServices([serviceId]) }
      .flatMap { $0.discoverCharacteristics([characteristicId])}
      .subscribe(onNext: { characteristic in
      print("Characteristic discovered: \(characteristic)")
      })
    

    RxBluetoothKit还具有正确执行连接恢复功能,监控蓝牙状态和监视单个外设连接状态的功能。

    何去何从? 327

    在本章中,您将了解如何实现和封装Apple框架。 有时,抽象官方Apple 框架或第三方库是非常有用的,它可以更好地与RxSwift连接。没有真正的书面规则来说明什么时候需要抽象一个,但是如果框架满足以下一个或多个条件,建议应用这一策略:

    • 完成和失败信息使用回调
    • 使用很多代表异步返回信息
    • 框架需要与应用程序的其他RxSwift部分进行互操作

    您还需要知道框架是否对数据必须处理哪个线程有限制。 因此,在创建RxSwift包装之前,先阅读文档是一个好主意。 不要忘了寻找现有的社区扩展 - 或者,如果你已经写了一个,那么考虑与社区共享它!:]

    相关文章

      网友评论

        本文标题:RxSwift_v1.0笔记——17 Creating Cust

        本文链接:https://www.haomeiwen.com/subject/kwhnextx.html