介绍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包装之前,先阅读文档是一个好主意。 不要忘了寻找现有的社区扩展 - 或者,如果你已经写了一个,那么考虑与社区共享它!:]