上周我们开启了RxSwift的学习之旅, 从可观察序列–>过滤运算符–>映射运算符, 接下来我们来说说组合运算符. 说实话, 对于之前的内容的学习, 我觉得还是比较通俗易懂的, 但是这次的组合运算符相比之前在理解难易程度上又上了个档次, 本节我们就来攻克这一挑战吧! 代码见:github
本节所需的一些关于组合的基本知识已经更新到github代码中的playground文件中, 没有接触过响应式编程的同学请和之前一样先行在playground中了解概要以便更好的理解本文. 本节我们就通过案例逐步精讲组合操作符在实际开发时的作用.
这次的UI层面也不是特别复杂, 一个TableView的列表页和一个有Slider控制的TableView列表页:
本章是对于RxSwift 响应式编程实战 映射运算符进行升级的一节, 所以我们接着网络请求这块讲起, 我们先将模型类(EOCategory
` EOError
EOLocation
EOEvent`)添加到工程中.
fileprivate static let API = "https://eonet.sci.gsfc.nasa.gov/api/v2.1"
static let categoriesEndpoint = "/categories"
fileprivate static let eventsEndpoint = "/events"
fileprivate static func request(endpoint: String, query: [String : Any] = [:]) -> Observable<[String : Any]> {
do {
guard let url = URL(string: API)?.appendingPathComponent(endpoint), var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
throw EOError.invalidURL(endpoint)
}
components.queryItems = try query.flatMap { (key, value) in
guard let v = value as? CustomStringConvertible else {
throw EOError.invalidParameter(key, value)
}
return URLQueryItem(name: key , value: v.description)
}
guard let finalURL = components.url else {
throw EOError.invalidURL(endpoint)
}
let request = URLRequest(url: finalURL)
return URLSession.shared.rx.response(request: request).map { _, data -> [String : Any] in
guard let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []), let result = jsonObject as? [String : Any] else {
throw EOError.invalidJSON(finalURL.absoluteString)
}
return result
}
} catch {
return Observable.empty()
}
}
我们从请求的函数开始讲起, 和之前不同的是, 这次我们将请求直接封装在模型里, 传统的MVC模式.
- 通过对
API
接口进行校验得到可用的URLComponents
, 如果url不可用抛出异常. - 将参数赋值给
URLQueryItem
, 如果value
不符合规则抛出异常. - 拿到最终的
URL
, 如果拿不到抛出异常. - 将
URL
转换成URLRequest
. - 进行JSON序列化, 返回
(HTTPURLResponse, Data)
中的Data
. - 如果抛出异常则返回空的可观察序列.
static var categories: Observable<[EOCategory]> = {
return EONET.request(endpoint: categoriesEndpoint)
.map { data in
let categories = data["categories"] as? [[String: Any]] ?? []
return categories
.flatMap(EOCategory.init)
.sorted { $0.name < $1.name }
}
.shareReplay(1)
}()
...
fileprivate static func events(forLast days: Int, closed: Bool, endpoint: String) -> Observable<[EOEvent]> {
return request(endpoint: eventsEndpoint, query: [
"days": NSNumber(value: days),
"status": (closed ? "closed" : "open")
]).map { json in
guard let raw = json["events"] as? [[String : Any]] else {
throw EOError.invalidJSON(endpoint)
}
return raw.flatMap(EOEvent.init)
}
}
接着我们对categories的外部变量进行计算属性的get方法.
- 进行请求调用上面的请求并传入尾部节点.
- 进行排序并
map
映射到EOCategory
模型上. shareReplay(1)
将请求进行一次缓存, 下次调用订阅不再进行请求.events
和categories
相同, 进行请求映射.
static func events(forLast days: Int = 360) -> Observable<[EOEvent]> {
let openEvents = events(forLast: days, closed: false)
let closedEvents = events(forLast: days, closed: true)
return openEvents.concat(closedEvents)
}
- 将
closedEvents
进行请求映射后的模型数组添加到openEvents
之后.
static func events(forLast days: Int = 360, category: EOCategory) -> Observable<[EOEvent]> {
let openEvents = events(forLast: days, closed: false, endpoint: category.endpoint)
let closedEvents = events(forLast: days, closed: true, endpoint: category.endpoint)
return Observable.of(openEvents, closedEvents).merge().reduce([]) { running, new in
running + new
}
}
- 进行
marge
, 用工git
的同学一定知道, 其效果和concat
类似.
func startDownload() {
download.progress.progress = 0.0
download.label.text = "Download: 0%"
let eoCategories = EONET.categories
let downloadedEvents = eoCategories.flatMap { categories in
return Observable.from(categories.map { category in
EONET.events(forLast: 360, category: category)
})
}.merge(maxConcurrent: 2)
let updatedCategories = eoCategories.flatMap { categories in
downloadedEvents.scan(categories) { updated, events in
return updated.map { category in
let eventsForCategory = EONET.filteredEvents(events: events, forCategory: category)
if !eventsForCategory.isEmpty {
var cat = category
cat.events = cat.events + eventsForCategory
return cat
}
return category
}
}
}
eoCategories.concat(updatedCategories).bind(to: categories).addDisposableTo(disposeBag)
}
这段是我们本节的重头戏, 我们逐一来讲解下:
eoCategories
首先我们拿到请求到的数据EONET.categories
属性观察get进行请求, 这个之前说过了.downloadedEvents
通过flatMap
映射转换成merge
合并后的每个相对应的EOEvent
模型数组, 并发数设为2.updatedCategories
通过flatMap
映射进行对合并完的downloadedEvents
模型数组进行scan
扫描, 并重新组合新的category
内的events
模型数组.- 最后将更新后的数据
concat
添加并bind
绑定在categories
变量上就大功告成了.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "categoryCell")!
let category = categories.value[indexPath.row]
cell.textLabel?.text = "\(category.name) (\(category.events.count))"
cell.accessoryType = (category.events.count > 0) ? .disclosureIndicator : .none
return cell
}
- categories即为上面请求映射过滤组合后绑定的变量, 通过对Cell的自定义 就能够得到下面请求的列表了.
完成了第一个页面, 我们开始着下一个页面: EventsViewController
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 60
- 进行Autolayout约束后, 我们可以在添加以下代码来代替之前复杂的cell高度的运算.
events.asObservable().subscribe(onNext: { [weak self] _ in
self?.tableView.reloadData()
}).addDisposableTo(disposeBag)
Observable.combineLatest(days.asObservable(), events.asObservable()) { (days, events) -> [EOEvent] in
let maxInterval = TimeInterval(days * 24 * 3600)
return events.filter { event in
if let date = event.closeDate {
return abs(date.timeIntervalSinceNow) < maxInterval
}
return true
}
}.bind(to: filteredEvent).addDisposableTo(disposeBag)
filteredEvent.asObservable().subscribe(onNext: { [weak self] _ in
self?.tableView.reloadData()
}).addDisposableTo(disposeBag)
days.asObservable().subscribe(onNext: { [weak self] days in
self?.daysLabel.text = "Last \(days) days"
}).addDisposableTo(disposeBag)
events
是从上个页面传递过来的变量, 类型为let events = Variable<[EOEvent]>([])
- 通过对
events
的作为可观察序列并进行订阅, 当可观察者被添加进订阅就进行列表的刷新. Observable.combineLatest()
对日期和时间做最后的绑定, 只保留可观察序列的最后的值的组合,并进行过滤绑定在filteredEvent
变量上, 类型是let filteredEvent = Variable<[EOEvent]>([])
- 接下来两行对于关注本系列的同学应该不用解释, 就是进行订阅并刷新UI.
@IBAction func sliderAction(_ sender: AnyObject) {
days.value = Int(slider.value)
}
最后对slider添加事件, 并改变days的值.接下来会触发一系列的订阅.
days.asObservable().subscribe(onNext: { [weak self] days in
self?.daysLabel.text = "Last \(days) days"
}).addDisposableTo(disposeBag)
- 触发上面的订阅并进行
UILabel
的UI刷新
Observable.combineLatest(days.asObservable(), events.asObservable()) { (days, events) -> [EOEvent] in
let maxInterval = TimeInterval(days * 24 * 3600)
return events.filter { event in
if let date = event.closeDate {
return abs(date.timeIntervalSinceNow) < maxInterval
}
return true
}
}.bind(to: filteredEvent).addDisposableTo(disposeBag)
- 触发
combineLatest
最新组合运算符, 并重新进行过滤绑定.
events.asObservable().subscribe(onNext: { [weak self] _ in
self?.tableView.reloadData()
}).addDisposableTo(disposeBag)
- 在对
event
过滤的过程中触发订阅进行列表刷新.
filteredEvent.asObservable().subscribe(onNext: { [weak self] _ in
self?.tableView.reloadData()
}).addDisposableTo(disposeBag)
- 在过滤完成重新绑定后, 触发订阅进行最后的刷新.
本文因为篇幅所限, 仅保留一些核心的代码, 并对核心代码进行逐条讲解, 需要详细了解, 请去github下载源码后对照阅读. 通过对于可观察序列, 过滤, 映射, 组合的理解和实战, 通过一个事件的改变异步触发订阅的响应式编程的思想, 我们应该已经能算入门了.
演示效果:
About:
🌟 源码 请点这里🌟 »> 喜欢的朋友请点喜欢 »> 下载源码的同学请送下小星星 »> 有闲钱的壕们可以进行打赏 »> 小弟会尽快推出更好的文章和大家分享 »> 你的激励就是我的动力!!