一路走来, 感觉RxSwift也不像之前一样晦涩难懂了, 甚至渐渐的喜欢上了这种响应式编程的思想, 将对象作为可观察对象并进行订阅, 加上过滤操作符的协作, 一切的逻辑运算都在后台线程执行, 本节所要讲的是Rx中最为强大的功能 — 映射, 代码见:github
本节所需的一些关于映射的基本知识已经更新到github代码中的playground文件中, 没有接触过响应式编程的同学请和之前一样先行在playground中了解概要以便更好的理解本文. 本节我们就通过案例逐步精讲映射操作符在网络请求时的妙用.
这次的UI界面非常的简单, 就是一个简单的TableViewController, 我们通过网络请求将获取的JSON数据映射到Model上并展示在Cell上, API: https://api.github.com/repos/coderZsq/coderZsq.target.swift/events
想了解如何进行后台搭建并模拟数据返回的同学 请点击 –> Hybird 实现热修复架构 从MVC说起
{
"id": "5715074941",
"type": "PushEvent",
"actor": {
"id": 19483268,
"login": "coderZsq",
"display_login": "coderZsq",
"gravatar_id": "",
"url": "https://api.github.com/users/coderZsq",
"avatar_url": "https://avatars.githubusercontent.com/u/19483268?"
},
"repo": {
"id": 87806175,
"name": "coderZsq/coderZsq.target.swift",
"url": "https://api.github.com/repos/coderZsq/coderZsq.target.swift"
},
"payload": {
"push_id": 1688506435,
"size": 1,
"distinct_size": 1,
"ref": "refs/heads/master",
"head": "90306f546ee0e17d94891415a2f8938d28736dc7",
"before": "778ac2b60b85a3e97e0a1e85e16cdb1061445181",
"commits": [
{
"sha": "90306f546ee0e17d94891415a2f8938d28736dc7",
"author": {
"email": "a13701777868@yahoo.com",
"name": "Castie!"
},
"message": "Update README.md",
"distinct": true,
"url": "https://api.github.com/repos/coderZsq/coderZsq.target.swift/commits/90306f546ee0e17d94891415a2f8938d28736dc7"
}
]
},
"public": true,
"created_at": "2017-04-19T12:12:49Z"
}
我们先从浏览器看下接口所返回的字段, 并提取我们需要的字段生成模型Model, 以备之后的映射.
typealias AnyDict = [String: Any]
class Event {
let repo: String
let name: String
let imageUrl: URL
let action: String
init?(dictionary: AnyDict) {
guard let repoDict = dictionary["repo"] as? AnyDict,
let actor = dictionary["actor"] as? AnyDict,
let repoName = repoDict["name"] as? String,
let actorName = actor["display_login"] as? String,
let actorUrlString = actor["avatar_url"] as? String,
let actorUrl = URL(string: actorUrlString),
let actionType = dictionary["type"] as? String
else { return nil }
repo = repoName
name = actorName
imageUrl = actorUrl
action = actionType
}
var dictionary: AnyDict {
return [
"repo" : ["name": repo],
"actor": ["display_login": name, "avatar_url": imageUrl.absoluteString],
"type" : action
]
}
}
我们将模型命名为Event, 添加了一个dictionary变量供返回序列化完成后的字典.
func fetchEvents(repo: String) {
let response = Observable.from([repo]).map { urlString -> URL in
return URL(string: "https://api.github.com/repos/\(urlString)/events")!
}.map { [weak self] url -> URLRequest in
var request = URLRequest(url: url)
if let modifiedHeader = self?.lastModified.value {
request.addValue(modifiedHeader as String,forHTTPHeaderField: "Last-Modified")
}
return request
}.flatMap { request -> Observable<(HTTPURLResponse, Data)> in
return URLSession.shared.rx.response(request: request)
}.shareReplay(1)
response.filter { response, _ in
return 200..<300 ~= response.statusCode
}.map { _, data -> [[String: Any]] in
guard let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []),
let result = jsonObject as? [[String: Any]] else { return [] }
return result
}.filter { objects in
return objects.count > 0
}.map { objects in
return objects.flatMap(Event.init)
}.subscribe(onNext: { [weak self] newEvents in
self?.processEvents(newEvents)
}).addDisposableTo(bag)
}
一切准备就绪, 我们就来说说本节最重要的部分了, 我们逐行来进行讲解.
- 通过
Observable.from()
的方式创建一个可观察对象, 传入repo
字段, 通过map
映射成URL
返回. - 通过
map
映射将URL
映射成URLRequest
, 如果有进行修改则写入请求头. - 通过’flatmap’映射将将
URLRequest
映射成HTTPURLResponse
, 网络请求封装在RxCocoa
的URLSession.shared.rx.response(request:)
方法中, 有兴趣的同学可以看下源码, 当然这些都是在后台线程中执行的. - 最后通过
shareReplay(1)
进行订阅共享.shareReplay
可以将之前响应结果加入缓冲区. 这也是不使用share()
的原因, 避免多次请求.
对于flatmap 和 map 之前的区别不了解的同学可以看下playground中的例子, 或者直接查看Swift中的Array文档.
/// Returns an array containing the non-`nil` results of calling the given
/// transformation with each element of this sequence.
///
/// Use this method to receive an array of nonoptional values when your
/// transformation produces an optional value.
///
/// In this example, note the difference in the result of using `map` and
/// `flatMap` with a transformation that returns an optional `Int` value.
///
/// let possibleNumbers = ["1", "2", "three", "///4///", "5"]
///
/// let mapped: [Int?] = numbers.map { str in Int(str) }
/// // [1, 2, nil, nil, 5]
///
/// let flatMapped: [Int] = numbers.flatMap { str in Int(str) }
/// // [1, 2, 5]
///
/// - Parameter transform: A closure that accepts an element of this
/// sequence as its argument and returns an optional value.
/// - Returns: An array of the non-`nil` results of calling `transform`
/// with each element of the sequence.
///
/// - Complexity: O(*m* + *n*), where *m* is the length of this sequence
/// and *n* is the length of the result.
public func flatMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]
拿到了响应结果, 我们对相应结果进行过滤, 如果对过滤不了解的同学, 请翻看之前的文章 –> RxSwift 响应式编程实战 过滤运算符
- 通过
filter
过滤掉错误的statusCode, 对于statusCode不了解的同学可以查阅HTTP状态码的资料,~=
操作符是指操作符右边的值是否在操作符左边的范围之内, 返回Bool类型, - 得到正确的数据后, 通过
map
进行映射将数据映射成字典数组类型, 使用的是系统的JSON序列化. - 再次进行
filter
过滤, 过滤掉没有返回的数据. - 再次进行
map
映射, 将数据结构映射到Event模型中. - 最后进行订阅执行操作并添加到
DisposeBag
中.
接着我们来看下拿到了映射完成后的[Event]模型数组后的执行操作.
func processEvents(_ newEvents: [Event]) {
var updatedEvents = newEvents + events.value
if updatedEvents.count > 50 {
updatedEvents = Array<Event>(updatedEvents.prefix(upTo: 50))
}
events.value = updatedEvents
tableView.reloadData()
refreshControl?.endRefreshing()
let eventsArray = updatedEvents.map{ $0.dictionary } as NSArray
eventsArray.write(to: eventsFileURL, atomically: true)
}
- 拿到模型数组的个数后取如果超过50个, 取前面50个数据.
- 刷新tableView, refreshControl结束刷新, 并将映射模型缓存到沙盒.
func cachedFileURL(_ fileName: String) -> URL {
return FileManager.default.urls(for: .cachesDirectory, in: .allDomainsMask).first!.appendingPathComponent(fileName)
}
class ViewController: UITableViewController {
fileprivate let repo = "coderZsq/coderZsq.target.swift"
fileprivate let events = Variable<[Event]>([])
fileprivate let bag = DisposeBag()
fileprivate let eventsFileURL = cachedFileURL("events.plist")
fileprivate let modifiedFileURL = cachedFileURL("modified.txt")
fileprivate let lastModified = Variable<NSString?>(nil)
override func viewDidLoad() {
super.viewDidLoad()
title = repo
let eventsArray = (NSArray(contentsOf: eventsFileURL)
as? [[String: Any]]) ?? []
events.value = eventsArray.flatMap(Event.init)
lastModified.value = try? NSString(contentsOf: modifiedFileURL, usedEncoding: nil)
setupRefreshControl()
}
}
cachedFileURL
缓存路径地址- 每次先执行缓存本地的映射数据再进行网络请求.
映射和过滤的好处在于一切都在后台线程进行, 并在映射和过滤的过程中如果发生不匹配的情况, 之后的操作就会被打断, 不再执行.
演示效果:
About:
🌟 源码 请点这里🌟 »> 喜欢的朋友请点喜欢 »> 下载源码的同学请送下小星星 »> 有闲钱的壕们可以进行打赏 »> 小弟会尽快推出更好的文章和大家分享 »> 你的激励就是我的动力!!