RxSwift by Examples 分成 4 部分。以下是 PART 1 的学习笔记和翻译整理。原文在这里。
RxSwfit 是用 Swift 来实现的一个响应式拓展。ReactiveX 是观察者模式、迭代者模式和函数式编程的优点组合。
你需要根本上改变你的视野,从静态地分配一个值给变量,到观察将可能在未来会改变的某物。
为什么要使用它?答案是:简单。用信号 signals 取代难以测试的通知 notifications。用 block 取代 delegate,委托要将代码写在多个地方,block 还可以移除大量的 switch/if。Rxswift 平滑地处理了 KVO、IBAction, 输入 filter、MVVM 等等。
术语
你的手机是一个可被观察者 observable,它发出信号,比如 Facebook 的通知消息。你订阅了它,所以你可以从 home 屏幕收到每个推送通知。于是你可以决定如何处理这些信号 signal。你是一个观察者。
例子
我们将写一个 City Searcher,在搜索框中输入城市,动态地显示列表。当你在 search bar 中输入的时候,我们动态地获取以输入字母开头的城市并显示在 table view 中。
似乎很简单是吗?当你试着创建动态搜索时,你不得不考虑异常的情况。比如说我输入非常快并且经常改变想法?我们会有很多 api 请求需要过滤。在真实的 app 中,你需要取消之前的请求,在发出另一个请求之前等待一段时间,检查输入是否和之前的一样,等等。咋看似乎很简单的任务创造了大量的逻辑。
当然没有 Rx 你也可以做到,但是我们看看如何用一点点代码实现这些逻辑。
Podfile
创建项目并用 pod 安装 RxSwift + RxCocoa
platform :ios, '8.0'
use_frameworks!
target 'RxSwiftExample' do
pod "RxSwift"
pod "RxCocoa"
end
创建UI
UISearchBar + UITableView
变量
为了减少代码逻辑我们先不用 API 请求。我们需要两个数组,一个存所有城市,另一个存需要显示的城市。
var shownCities = [String]() // Data source for UITableView
let allCities = ["New York", "London", "Oslo", "Warsaw", "Berlin", "Praga"] // Our mocked API data source
然后我们需要设置 UITableViewDataSource 并连接到变量 shownCities。
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var searchBar: UISearchBar!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return shownCities.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cityPrototypeCell", for: indexPath)
cell.textLabel?.text = shownCities[indexPath.row]
return cell
}
观察
开始更有趣的部分。我们将观察 UISearchaBar 中的 text。RxCocoa 已经为我们创建好了。Rx 团队支持许多 Cocoa 框架,包括 UISearchBar 和许多控制器。在我们的例子中,我们使用 UISearchBar,我们可以使用它的属性 rx.text,每次 search bar 中的 text 改变的时候,它就会发出信号。怎样观察这个东西呢?首先引入 RxCocoa 和 RxSwift
import RxCocoa
import RxSwift
进入观察部分:在 viewDidLoad() 中,我们为 UISearchBar 添加对 rx.text 属性的观察。
searchBar
.rx.text // Observable property thanks to RxCocoa
.orEmpty // Make it non-optional
.subscribe(onNext: { [unowned self] query in // Here we will be notified of every new value
self.shownCities = self.allCities.filter { $0.hasPrefix(query) } // We now do our "API Request" to find cities.
self.tableView.reloadData() // And reload table view data.
})
.addDisposableTo(disposeBag)
动态搜索已经做好了。subscribe 很好理解 —— 我们订阅了可观察的 property,它产生信号。就像你告诉你的手机:每次收到新消息请显示给我。它将显示所有最新的值,这是我们唯一需要的,不过 subscribe 还包装了很多事件比如 onError, onCompleted 等等。
更有意思的是最后一行。当你订阅了 observables,你通常想要在这个对象被销毁 deallocated 的时候取消订阅。在 Rx 中用 DisposeBag 装载所有你想要在 deinit 进程中取消订阅的东西。通常你需要创建这个 bag 并把废弃物丢给它。
var shownCities = [String]() // Data source for UITableView
let allCities = ["New York", "London", "Oslo", "Warsaw", "Berlin", "Praga"] // Our mocked API data source
let disposeBag = DisposeBag() // Bag of disposables to release them when view is being dea
优化
编译后我们就有一个正常工作的 app 了。但是我们所担心的那些事情呢?泛滥的 api 请求?空输入?延迟?对,我们需要保护自己,保护我们的 api 后端。我们需要加延迟,在输入 x 秒之后发出请求,并且仅在输入改变的情况下。通常我们需要创建一个 NSTimer 来 fire 和 delay 后 invalidate 它,如果有新的输入。
当我们输入 O,结果显示,然后改变主意输入 oc,然后迅速又改回 o,在延迟之前,api 请求发出。这时候我们发出了两个同样的请求。通常我们需要延迟 0.5 秒。没有 Rx 的话,我们需要添加 flag 给搜索请求,并且对比它是否与新的请求一样。随着逻辑的不断增加,这有太多代码要写了。在 RxSwift 中做这个事情只需要两行。
debounce()
制造延迟效果,distinctUntilChanged()
在值相同的时候保护我们。
searchBar
.rx.text // Observable property thanks to RxCocoa
.orEmpty // Make it non-optional
.debounce(0.5, scheduler: MainScheduler.instance) // Wait 0.5 for changes.
.distinctUntilChanged() // If they didn't occur, check if the new value is the same as old.
.subscribe(onNext: { [unowned self] query in // Here we subscribe to every new value
self.shownCities = self.allCities.filter { $0.hasPrefix(query) } // We now do our "API Request" to find cities.
self.tableView.reloadData() // And reload table view data.
})
.addDisposableTo(disposeBag)
但是我们还忘了一件事。我们输入一些值,刷新了 table view,然后又删除这些输入的时候呢?我们发出了空参数的请求。Swift 已经内建了 filter()
函数。为什么我要在一个值中使用 filter 呢?filter()
不是作用于集合吗?嗯,不要把可观察者 Observable 看作一个值/对象,它是一系列值的流,这样你将很容易理解函数式 block 的使用。为了过滤值我们对待它如同对待 string 数组一样。
.filter { !$0.isEmpty } // Filter for non-empty query.
完整的代码只需要 9 行,实现了并不简单的逻辑。
searchBar
.rx.text // Observable property thanks to RxCocoa
.orEmpty // Make it non-optional
.debounce(0.5, scheduler: MainScheduler.instance) // Wait 0.5 for changes.
.distinctUntilChanged() // If they didn't occur, check if the new value is the same as old.
.filter { !$0.isEmpty } // If the new value is really new, filter for non-empty query.
.subscribe(onNext: { [unowned self] query in // Here we subscribe to every new value, that is not empty (thanks to filter above).
self.shownCities = self.allCities.filter { $0.hasPrefix(query) } // We now do our "API Request" to find cities.
self.tableView.reloadData() // And reload table view data.
})
.addDisposableTo(disposeBag)
结束。