距离我 上一篇文章 现已很久了。我想写一篇文章,介绍 Observable
背面的理论,并展现在实在而非虚拟示例中的用法。事实证明,这并不是我想象中的小事。
文章分为两部分。一开端,我试图解说什么是 Observable
以及怎么处理它们。
第二部分是怎么运用 RxSwift 完成 Spotify 曲目查找的教程。理论归理论,实例却能让人更简单了解。
长话短说,期望大家喜爱阅览
Observable – 序列
在运用 Rx 时,阻止人们深入其中的原因是指令式的考虑方法。你有必要打破旧习惯,开端用 Rx 的方法考虑。
我主张你像考虑数组相同考虑 Observable
。这是因为,你能够像运用数组相同对 Observable
进行 map
、filter
或者 reduce
。经过 map
,你能够将 Observable<String>
转化为 Observable<Bool>
,就像将 Array<String>
转化为 Array<Bool>
相同。
不过,Observable
和数组之间有一个巨大的差异。
你知道正方形和立方体是什么吧?它们是怎么彼此参照的?立方体便是在正方形的基础上增加了一个维度——深度。我喜爱这样描绘 Observable
:它是一个数组,多了一个维度——时刻。
当你有一个包括 5 个元素的 Array<String>
时,你能够访问任何你想要的元素。一切元素在时刻 t0 时都可用。
另一方面,当你有一个 Observable<String>
时,第一个元素只在 t1 时刻可用,第二个在 t2 时刻可用,第三个在 t3 时刻可用,依此类推。更重要的是,在 tn 时刻内,你无法获取任何从前或未来的元素。
迭代器和调查者形式
在计算机科学中,迭代器形式十分常用。在 Swift 中,迭代器形式由 IteratorProtocol 完成(在 Swift 3.0 之前是 GeneratorType
)。
迭代器只有一个函数 next()
。经过迭代器,能够遍历序列中的一切元素。每个 Sequence 和数组相同,都完成了 IteratorProtocol
。
另一方面,Observable
混合了两种设计形式。迭代器形式和调查者形式。混合这两种形式的成果是,当你处理 Observable
时,你能够监听 “next” 事情。
你不得不供认,这两者有异曲同工之妙,不是吗?
然而,国际是由小细节构建而成的。我想在这儿指出的是,在数组中,接收者担任何时获取下一个元素。
而在 Observable
中,生产者担任何时发送下一个元素。接收者只能监听它们。
map 示例
一开端我说过,你能够像映射数组相同映射 Observable
:
let names = ["adam", "daniel", "christian"]
let nameLengths = names.map { $0.characters.count }
print(nameLengths) // "4, 6, 9"
需求留意的是,nameLengths
是一个独自的数组,称号不会改变。同样的状况也会产生在 Observable
上:
let disposeBag = DisposeBag()
let backgroundScheduler = SerialDispatchQueueScheduler(globalConcurrentQueueQOS: .userInitiated)
//(1) This Observable emits names in 1 second interval.
let asyncNames: Observable<String> = self.createObservable(from: ["adam", "daniel", "christian"], withTimeInterval: 1)
//(2) map creates new independant Observable<Int>
let asyncNameLenghts: Observable<Int> = asyncNames
.map { name in
return name.characters.count
}
asyncNameLenghts.subscribe(onNext: { count in
print("[(NSDate())]: (count)")
}).disposed(by: disposeBag)
/*
#################################################
[2016-10-31 08:15:27 +0000]: 4
[2016-10-31 08:15:28 +0000]: 6
[2016-10-31 08:15:29 +0000]: 9
*/
在数组中运用 map
会立即转化一切元素,而在 Observable
中运用 map
也会转化序列中的一切项目,但转化会在新事情产生时独自应用。
在上面的示例中,你看到 subscribe
办法了吗?subscribe
是运用 Observable
时的一个十分重要的办法。subscribe
告诉 Observable
“嘿,我准备好监听你的元素了”。假如忘掉调用 subscribe
,就不会有任何事情产生。
Observable – 依据事情的序列
到现在中止,我一直在运用 “next” 这个词来调用在 Observable
序列中发送的事情。事实上,Observable
会宣布事情(events)。Next 仅仅枚举中的一种特别类型。Observable
能够带着三种类型的事情,分别是:next
、error
和 completed
:
public enum Event<Element> {
case next(Element)
case error(Swift.Error)
case completed
}
我以为 next
不言自明。假如你有一个 Observable<Person>
,那么每个 next
事情都会带着一个 Person
实例。
因为 Observable
是一个具有时刻维度的序列,因而调查者不知道序列的巨细,这意味着他不知道何时中止监听事情。这便是 error
和 completed
存在的目的。
出错时会发送 error
事情,当 Observable
想告诉它的调查者不再发送事情时会发送 completed
事情。
请记住 Observable
的另一个重要特性:error
和 completed
总是中止 Observable
序列,因而它不会再发送任何新事情。
RxMarbles
map
仅仅现有操作符中一个常用的比如。除此之外,Observable
还有更多的操作符:
-
map
-
flatMap
-
flatMapLatest
-
withLatestFrom
-
filter
-
debounce
-
retry
-
skip
-
catchError
-
take
-
concat
-
merge
上面列出的是我在日常开发中最常用的一组 Rx 操作符。你或许会说它很大,但我想说,与可用的操作符集比较,它依然很小:D。
为了使操作符易于了解,ReactiveX 为你提供了名为 RxMarbles 的图表。这些图表示 Observable
序列以及操作符怎么影响它。我向你展现的 map
示例的 RxMarble 能够如下所示:
…
弹珠图例
因为 Observable
能够宣布 3 种类型的事情,因而 RxMarbales 中也有 3 种类型的符号,分别代表 next
、error
和 completed
:
另一种制作弹珠图的办法是 ASCII 格式。这种方法在 stackoverflow、论坛或 slack 上很常见:
----A--D---C---|->
# map(get string size) #
----4--6---9---|->
---1------X------>
A, B, D, 1, 6, 9 - are next events
| - is the completed event
X - is the error event
--------> - is a timeline
此外,你还能够在 rxmarbles.com 网站上以可调查序列操作弹珠,并查看运算成果。它还能够作为 iPhone 应用程序在 App Store 上下载。肯定值得一试;)。
Spotify 查找示例
没有什么能比一个好比如更清楚地说明问题了。我的示例包括了反应式编程的基本概念。使命很简单。你有必要完成与 Spotify Search API 交互的 SearchViewController
。
要下载初始项目设置,请点击此处
需求
为了让本教程不那么琐碎,我期望你能包括那些我以为在 iOS 开发人员的实际生活中或许会呈现的需求:
- 不要在用户每次输入时都向 Spotify 发送恳求。等候 0.3 秒,直到他输完中止;
- 在开端时预载 “Let it go – frozen” 作为特征查询;
- 当用户更改查询时,铲除之前的查找成果;
- 用户能够经过 “下拉改写” 功用改写当时查找成果;
- 查询有必要至少包括 3 个字母才干进行查找;
RxMantra – 一切都能够是序列
在编写任何代码之前,我期望你首要考虑序列和图表。之后,你就能够开端考虑代码了。记住,一切都能够是序列。这是 ReactiveX 的口号。
要求 #0 – 下载和显现曲目
首要要完成的是为给定的查询下载曲目。你需求将来自查找栏的查询转化为一组曲目。记住,首要要考虑序列!
在 “Observable 语言” 中,你有必要将一个 Observable<String>
转化为 Observable<[Track]>
。正如你在示例代码中看到的,TrackCell
将 TrackRenderable
作为输入来烘托自己。这意味着你有必要转化 Observable<String> -> Observable<[Track]> -> Observable<[TrackRenderable]>
:
正如你所看到的,[Track]
事情会依据查询事情在时刻上移动。这是因为 [Track]
来自 Spotify API,它需求一些时刻来树立连接,所以呼应会晚一点。
你是否问过自己,怎么才干将 query
映射到 [Track]
?到现在中止,咱们现已运用了同步回来值的 map
,但现在有必要异步转化它。
flatMap
我想介绍flatMap
操作符。flatMap
回来一个 Observable
,并将其一切事情转移到原始序列中。
现在咱们来看看这些图的代码是怎样的:
searchBar.rx.text.orEmpty
.flatMap { [spotifyClient] query in
return spotifyClient.rx.search(query: query)
}.map { tracks in
return tracks.map(TrackRenderable.init)
}
要在 UITableView
中显现 [TrackRenderable]
,需求将调查对象与 tableView
绑定:
searchBar.rx.text.orEmpty
.flatMap { [spotifyClient] query in
return spotifyClient.rx.search(query: query)
}.map { tracks in
return tracks.map(TrackRenderable.init)
}.bindTo(tableView.rx.items(cellIdentifier: "TrackCell", cellType: TrackCell.self)) { index, track, cell in
cell.render(trackRenderable: track)
}.disposed(by: disposeBag)
要求 #1 – 延迟恳求
当时的 Observable
序列会在用户输入新内容时转化查询。能够经过将查询事情延迟到用户中止编写整个查询时进行改善。这种需求的弹珠图如下所示:
searchBarText: -A-B-C---D-E-F-G-H---I-J----->
delayedQuery: -----C-----------H-----J----->
幸运的是,RxSwift 有一个 debounce
操作符,它的效果与上图所示完全相同。它从 Observable
中获取最终一个事情,假如在经过的时刻间隔内没有发送新事情,就会发送该事情。
searchBar.rx.text.orEmpty
.debounce(.milliseconds(300), scheduler: MainScheduler.instance)
.flatMap { [spotifyClient] query in
return spotifyClient.rx.search(query: query)
}.map { tracks in
return tracks.map(TrackRenderable.init)
}.bindTo(tableView.rx.items(cellIdentifier: "TrackCell", cellType: TrackCell.self)) { index, track, cell in
cell.render(trackRenderable: track)
}.disposed(by: disposeBag)
在上一篇文章中,我介绍了本教程中要用到的几个操作符。我还展现了怎么在 Observable
中封装 API 调用。假如你不知道 spotifyClient 是怎么工作的,我主张你阅览这篇文章 。
中止之前的恳求
此刻,你应该具有一个 SearchViewController
,当用户输入任何内容时,它将显现专辑。恭喜,你现已迈出了运用 RxSwift 的第一步。
当然,现在的完成还不足以在应用程序商店发布。不幸的是,其中存在一个 bug。
假设用户刚输入 “let” 时,应用程序就开端下载曲目。在恳求结束前,用户完成了他的意图,并输入了 “let it go”。对 “let it go” 恳求的呼应比对 “let” 的呼应更早抵达。
因而,应用程序将首要显现 “let it go” 的成果。当 “let it go” 的呼应抵达时,应用程序将显现错误的旧查询成果。或许图表更简单了解:
因为网络恳求存在延迟,当“晚查询的内容”先回来,“从前恳求的内容”延迟回来时怎么处理?
searchRequest: --a-b---------c-->
## flatMap {...}
searchResponse: -----B-A-------C->
a,b,c - requests
A,B,C - responses for corresponding requests (A is response for a)
运用 flatMapLatest
flatMapLatest
是另一个很酷的操作符。它用于替代 flatMap
。它们之间的差异在于,flatMapLatest
会取消对闭包内部运用的任何曾经的 Observable
的订阅。有了它,新数据将永远不会被旧呼应所替代:
searchRequest: --a--b--------c-->
## flatMapLatest {...}
searchResponse: ------B--------C->
a,b,c - requests
A,B,C - responses for corresponding requests (A is response for a)
在代码中,只需更改 flatMap
为 flatMapLatest
即可:
searchBar.rx.text.orEmpty
.debounce(.milliseconds(300), scheduler: MainScheduler.instance)
.flatMapLatest { [spotifyClient] query in
return spotifyClient.rx.search(query: query)
}.map { tracks in
return tracks.map(TrackRenderable.init)
}.bindTo(tableView.rx.items(cellIdentifier: "TrackCell", cellType: TrackCell.self)) { index, track, cell in
cell.render(trackRenderable: track)
}.disposed(by: disposeBag)
需求 2 – 特征查询
下一个要求是,应用程序应在视图加载时加载 “Let it go – frozen” 成果。
总是先考虑弹珠图;):
searchBar.rx.text ----a---b-c-----d->
# ???????????
query F---a---b-c-----d->
a, b, c, d - string events representing searchBar text
F - featured query
你或许猜到了,Rx 为此提供了另一个操作符,那便是 startWith()
。 startWith()
的效果正是你想要做的。它接收一个参数,并将其作为 Observable
的初始值发送:
searchBar.rx.text ----a---b-c-----d->
# startWith(F)
query F---a---b-c-----d->
a, b, c, d - string events representing searchBar text
F - featured query string
o.O 屏幕依然是空的!
UISearchBar
、UITextField
或 UITextView
会向其 text Observable
上的新订阅者发送初始值。在咱们的比如中,searchBar
发送的是空字符串。应用程序内部产生的状况如下图所示:
searchBar.rx.text ""--a---b-c-----d->
# startWith(F)
query F-""--a---b-c-----d->
a, b, c, d - string events representing searchBar text
"" - empty string
F - featured query
序列以特征查询开端,但在特征查询之后会呈现空字符串,从而加载新的空呼应。要防止这种状况产生,能够运用 skip(1)
操作符。它仅仅省掉给定数量的事情:
searchBar.rx.text ""--a---b-c-----d->
# skip(1)
----a---b-c-----d->
# startWith(F)
query F---a---b-c-----d->
a, b, c, d - string events representing searchBar text
"" - empty string from
F - featured query
现在,当你知道 skip
的效果后,就能够将其增加到代码中了:
searchBar.rx.text.orEmpty
.skip(1) // 越过前1个元素
.debounce(.milliseconds(300), scheduler: MainScheduler.instance)
.startWith("Let it go - frozen")
.flatMapLatest { [spotifyClient] query in
return spotifyClient.rx.search(query: query)
}.map { tracks in
return tracks.map(TrackRenderable.init)
}.bindTo(tableView.rx.items(cellIdentifier: "TrackCell", cellType: TrackCell.self)) { index, track, cell in
cell.render(trackRenderable: track)
}.disposed(by: disposeBag)
这儿也能够运用
.filter { !$0.isEmpty } // 过滤空值
操作符替代,或者运用 RxSwift 5 新增的compactMap()
操作符。
要求 3 – 铲除曾经的查找成果
下一个需求改善的当地是在查询产生改变时铲除查找成果。
关于与 tableView
绑定的 Observable
序列来说,这意味着什么?这意味着当用户在 searchBar
中写入新内容时,Observable<[TrackRenderable]>
需求发送空数组:
请看上图。因为运用了 debounce
操作符,运转 Spotify API 的 Observable
会在用户中止输入时发送事情。但是,咱们期望强制终究 Observable<[TrackRenderable]>
在用户输入任何内容时发送空数组。
上图中有一个关键点值得留意。trackRenderables
似乎是 emptyTracksOnTextChanged
和 spotifyResponse
的总和。
在 RxSwift 中,你能够将同一类型的多个调查对象合并为一个调查对象。这一切都要归功于 merge
操作符。
让咱们创立一个 Observable
,当用户在查找栏中写入任何内容时,它将发送 TrackRenderable
类型的空数组:
let clearTracksOnQueryChanged = searchBar.rx.text.orEmpty
.skip(1).map { _ in return [TrackRenderable]() }
要运用 merge
运算符,有必要首要重构当时代码:
let tracksFromSpotify: Observable<[TrackRenderable]> = searchBar.rx.text.orEmpty
.skip(1)
.debounce(.milliseconds(300), scheduler: MainScheduler.instance)
.startWith("Let it go - frozen")
.flatMapLatest { [spotifyClient] query in
return spotifyClient.rx.search(query: query)
}.map { tracks in
return tracks.map(TrackRenderable.init)
}
tracksFromSpotify.bindTo(tableView.rx.items(cellIdentifier: "TrackCell", cellType: TrackCell.self)) { index, track, cell in
cell.render(trackRenderable: track)
}.disposed(by: disposeBag)
有什么改变?我仅仅把 Observable
移到了变量中。现在你能够运用 merge
了:
Observable.of(tracksFromSpotify, clearTracksOnQueryChanged).merge()
.bindTo(tableView.rx.items(cellIdentifier: "TrackCell", cellType: TrackCell.self)) { index, track, cell in
cell.render(trackRenderable: track)
}.disposed(by: disposeBag)
它应该能够见效!当用户输入新的查询内容时,tableView
应该是空白的。
不过,我想进步可读性,并在将 Observable
与 tableView
绑定之前将其提取合并到一个独自的变量中。此外,我还发现有一处重复,我想去掉它:
let searchTextChanged = searchBar.rx.text.orEmpty.asObservable().skip(1)
let tracksFromSpotify = searchTextChanged
.debounce(.milliseconds(300), scheduler: MainScheduler.instance)
.startWith("Let it go - frozen")
.flatMapLatest { [spotifyClient] query in
return spotifyClient.rx.search(query: query)
}.map { tracks in
return tracks.map(TrackRenderable.init)
}
let clearTracksOnQueryChanged = searchTextChanged
.map { _ in return [TrackRenderable]() }
let tracks = Observable.of(tracksFromSpotify, clearTracksOnQueryChanged).merge()
tracks.bindTo(tableView.rx.items(cellIdentifier: "TrackCell", cellType: TrackCell.self)) { index, track, cell in
cell.render(trackRenderable: track)
}.disposed(by: disposeBag)
要求 #4 – 下拉改写
猜猜你一开端要做什么?是的,你有必要首要考虑弹珠图,并记住一切都能够是一个序列!我会反复强调这一点;)。
现在是第二个问题。你通常是怎么经过下拉改写完成查找的?我猜当用户滑动 tableView
时,你会从查找栏中获取当时文本并执行查找。
我说错了吗?当用户滑动 tableView
时,你需求重复之前的恳求。让咱们画一些图:
searchBar.rx.text --a---b----c-----d-----e--->
pullToRefreshObservable --------X-----X------X----->
queryOnPullToRefresh --------b-----c------d----->
requestObservable --a---b-b--c--c--d---d-e--->
responseObservable ----A---B-B--C--C--D--D-E-->
a,b,c,d,e - query from searchBar or request with the query
X - event when a user pulls the tableView
A,B,C,D,E - response for corresponding request with the query
现在,当你知道要找什么时,请访问 rxmarbles.com,找到你需求的操作符;)。
你回来了!这意味着你现已找到了 withLatestFrom
操作符,它的功用如上图所示。
一开端,你有必要在 tableView 中增加 UIRefreshControl
。办法是在 viewDidLoad
的最初增加这几行:
let refreshControl = UIRefreshControl()
tableView.addSubview(refreshControl)
好了,现在是创立 Observable
的时分了,它会在用户滑动 tableView
时发送事情:
let didPullToRefresh: Observable<Void> = refreshControl.rx.controlEvent(.valueChanged)
.map { [refreshControl] in
return refreshControl.isRefreshing
}.filter { $0 == true } // 过滤一切 false 值
.map { _ in return () }
在上面的代码中,我运用了 filter
运算符。期望从名字上就能了解。它会忽略一切假布尔值。
isRefreshing ---T--F-T---F----T->
# filter { $0 == T }
didPullToRefresh ---T----T---T----T->
下一个需求的 Observable
是当用户拉取 tableView 时重复前次查询的 Observable
:
let refreshWithLastQuery = didPullToRefresh
.withLatestFrom(searchTextChanged)
这很简单,不是吗?现在,你有必要将它与当时的 Observable
堆栈结合起来:
let didPullToRefresh: Observable<Void> = refreshControl.rx.controlEvent(.valueChanged)
.map { [refreshControl] in
return refreshControl.isRefreshing
}.distinctUntilChanged()
.filter { $0 == true }
.map { _ in return () }
let searchTextChanged = searchBar.rx.text.orEmpty.asObservable().skip(1)
let theQuery = searchTextChanged
.debounce(.milliseconds(300), scheduler: MainScheduler.instance)
.startWith("Let it go - frozen")
let refreshLastQuery = didPullToRefresh
.withLatestFrom(searchTextChanged)
let tracksFromSpotify = Observable.of(theQuery, **refreshLastQuery**).merge()
.flatMapLatest { [spotifyClient] query in
return spotifyClient.rx.search(query: query)
}.map { tracks in
return tracks.map(TrackRenderable.init)
}
let clearTracksOnQueryChanged = searchTextChanged
.map { _ in return [TrackRenderable]() }
let tracks = Observable.of(tracksFromSpotify, clearTracksOnQueryChanged).merge()
tracks.bindTo(tableView.rx.items(cellIdentifier: "TrackCell", cellType: TrackCell.self)) { index, track, cell in
cell.render(trackRenderable: track)
}.disposed(by: disposeBag)
躲藏 UIRefreshControl
还有一种状况需求考虑。当应用程序结束下载曲目时,你需求躲藏 UIRefreshControl
。
Observable
有一个特别的操作符,用于处理躲藏 UIRefreshControl
这样的副效果。我说的是 do(next:)
操作符。
do(next:)
不会从闭包回来任何内容。它仅仅确保在 next
事情产生时调用传递的闭包:
...
let tracksFromSpotify = Observable.of(theQuery, refreshLastQuery).merge()
.flatMapLatest { [spotifyClient] query in
return spotifyClient.rx.search(query: query)
}.map { tracks in
return tracks.map(TrackRenderable.init)
}.do(onNext: { [refreshControl] _ in
refreshControl.endRefreshing()
})
...
运转应用程序,看看有什么发现。你应该能看到 Spotify 查找和下拉改写功用。你还保护了自己,防止向 API 发送过多恳求。做得好
最终的话和作业
到此中止。教程到此结束。读完这篇文章后,我期望你记住什么是 Observable
。正如你所读到的,我喜爱把 Observable
描绘为一个数组,并增加了一个维度 — 时刻。重要的是,Observable
能够宣布 next
、error
和 completed
事情。
反应式编程是关于序列和事情的,它迫使咱们选用一种新的思想方法。遇到问题时,先画几个图,然后去 rxmarbles.com 查找所需的运算符。
还有一个要求咱们还没有完成。查询有必要至少包括 3 个字母才干开端查找成果。我期望你能增加这一功用;)。最终,你能够点击此处查看我的解决方案。
private lazy var query: Observable<String> = {
return self.searchText
.debounce(.milliseconds(300), scheduler: MainScheduler.instance)
.filter(self.filterQuery(containsLessCharactersThan: Constatnts.minimumSearchLength))
}()
// 这是对 filter 操作符 block 块的封装
private func filterQuery(containsLessCharactersThan minimumCharacters: Int) -> (String) -> Bool {
return { query in
return query.count >= minimumCharacters
}
}
敬请期待!
PS 假如你喜爱这篇文章,请共享给你的朋友们!
PS2 假如你有任何疑问或想给我反馈,请在下方留言。