Castie!

正态分布, 优劣伴生

北冥有鱼,其名为鲲(kūn)。鲲之大,不知其几千里也;化而为鸟,其名为鹏。鹏之背,不知其几千里也;怒而飞,其翼若垂天之云。是鸟也,海运则将徙于南冥。南冥者,天池也。


北海若曰:“井鼃不可以语于海者,拘于虚也;夏虫不可以语于冰者,笃于时也;曲士不可以语于道者,束于教也。今尔出于崖涘,观于大海,乃知尔丑,尔将可与语大理矣。

RxSwift 函数式组合运算符实操

上周我们开启了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)将请求进行一次缓存, 下次调用订阅不再进行请求.
  • eventscategories相同, 进行请求映射.
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:

点击下方链接跳转!!

🌟 源码 请点这里🌟 »> 喜欢的朋友请点喜欢 »> 下载源码的同学请送下小星星 »> 有闲钱的壕们可以进行打赏 »> 小弟会尽快推出更好的文章和大家分享 »> 你的激励就是我的动力!!

最近的文章

Animations 你真的会用View的动画吗?

RxSwift的学习我们先告一段落, 实在是太烧脑了, 今天我们换换脑子, 玩点轻松的动画, 去年我连载了一系列基于本人封装的Objective-C框架SQExtension展开的项目实战, 里面关于动画方面的涉及还是蛮多的以至于受到了腾讯的关注. 这个系列我们就来细讲动画, 一起走上进阶之路. 代码见:github对于动画方面除了最基本的View动画之外, 关于Layer的核心动画我都是看这篇wiki学的, 像去年连载的一些自定义转场动画是从Objc中学的. 这次买了Ray家的一系列...…

移动开发继续阅读
更早的文章

RxSwift 函数式映射运算符实操

一路走来, 感觉RxSwift也不像之前一样晦涩难懂了, 甚至渐渐的喜欢上了这种响应式编程的思想, 将对象作为可观察对象并进行订阅, 加上过滤操作符的协作, 一切的逻辑运算都在后台线程执行, 本节所要讲的是Rx中最为强大的功能 — 映射, 代码见:github本节所需的一些关于映射的基本知识已经更新到github代码中的playground文件中, 没有接触过响应式编程的同学请和之前一样先行在playground中了解概要以便更好的理解本文. 本节我们就通过案例逐步精讲映射操作符在网络...…

移动开发继续阅读