本文主要来自Getting Started With RxSwift and RxCocoa这篇文章
维基百科上有一个更明确的例子:
对于命令式编程,当a = b + c, a由b+c计算得到;但是之后,b和c发生了变化,这个变化对a并不生效,a还是保持原有的值;而当使用响应式编程时,当b和c发生变化,程序不需要再次执行a = b + c公式,a的值就会自动更新。
目前分为两派
相同点:
不同点:
关于这两个库的详细对比,见:ReactiveCocoa vs RxSwift
RxSwift: 响应式编程在Swift语言领域的实现库
RxCocoa: 针对Cocoa平台(iOS & Mac OS)的响应式编程库
多个Observer可以监听一个Observable,当发生变化时,所有Observer都会被通知
DisposeBag是RxSwift提供的处理ARC和内存管理的工具。销毁一个父对象时,会使得DisposeBag中的Observer对象同时销毁。
当持有DisposeBag的对象的deinit()
调用时,每个disposable Observer都会取消对监听对象的订阅,这时ARC就可以正常回收内存了。
如果没有DisposeBag的话,可能会出现两种情况,要么是observer会保留下来不被销毁,继续监听;要么被释放,造成崩溃。
建立Observer对象时,记得同时添加到DisposeBag中,来让DisposeBag帮助你回收该对象。
先下载示例工程
打开编译后即可看到如下界面
这个例子功能很简单:选择巧克力后,点击右上角可以结账,然后进行模拟支付。
在ChocolatesOfTheWorldViewController.swift中可以看到实现UITableViewDelegate
和UITableViewDataSource
的extension。
在观察一下updateCartButton()
这个方法,它用来更新购物车中的巧克力数量,在下面两种情况时调用:
这就是命令式编程方式实现:你必须手动调用方法来更新购物车巧克力数量。
购物车商品信息保存在ShoppingCart.sharedCart
这个单例中
定义在ShoppingCart.sharedCart中:
var chocolates: [Chocolate] = []
虽然可以给其定义中添加一个didSet
闭包,但问题是,这种做法只能在整个数组更新时才能被通知,而不是数组中任意元素变化就可以得到通知。
针对这种情况,RxSwift提供了解决方案,按照如下方式创建chocolates
变量:
let chocolates: BehaviorRelay<[Chocolate]> = BehaviorRelay(value: [])
使用RxSwift的BehaviorRelay
对象,持有一个Chocolate数组类型的值。这么做的目的是:通过BehaviorRelay
对象的asObservable()
可以得到一个observable,这样我们就可以添加监听者来订阅BehaviorRelay
对象的value
(chocolate数组)的变化。
上述做法的缺点是,对chocolate数组的修改必须修改为使用accept(_:)
,这是BehaviorRelay
为修改value
属性提供的方法。由于对数据的访问方式发生了变化,代码中相应的地方也要做修改。
在ShoppingCart.swift中
totalCost()
方法的修改:
return chocolates.reduce(0) {// 修改为return chocolates.value.reduce(0) {
itemCountString()
方法的修改:
guard chocolates.count > 0 else {// 修改为guard chocolates.value.count > 0 else {let setOfChocolates = Set<Chocolate>(chocolates)// 修改为let setOfChocolates = Set<Chocolate>(chocolates.value)let count: Int = chocolates.reduce(0) {// 修改为let count: Int = chocolates.value.reduce(0) {
在CartViewController.swift中
reset()
方法的修改:
ShoppingCart.sharedCart.chocolates = []// 修改为ShoppingCart.sharedCart.chocolates.accept([])
在ChocolatesOfTheWorldViewController.swift中
updateCartButton()
方法的修改:
// 修改为cartButton.title = "\(ShoppingCart.sharedCart.chocolates.value.count) \u{1f36b}"
tableView(_:didSelectRowAt:)
方法的修改:
ShoppingCart.sharedCart.chocolates.append(chocolate)// 修改为let newValue = ShoppingCart.sharedCart.chocolates.value + [chocolate]ShoppingCart.sharedCart.chocolates.accept(newValue)
完成上述修改后,我们就可以来对chocolates
添加observer了。
在ChocolatesOfTheWorldViewController.swift中,新增:
private let disposeBag = DisposeBag()
在//MARK: Rx Setup
的extension中添加:
func setupCartObserver() { //1 ShoppingCart.sharedCart.chocolates.asObservable() .subscribe(onNext: { //2 [unowned self] chocolates in self.cartButton.title = "\(chocolates.count) \u{1f36b}" }) .disposed(by: disposeBag) //3}
上述代码即可实现对购物车的自动更新。
可以看到,RxSwift大量使用函数链,就是说每一个函数接收前一个函数的结果。
对上述代码的解释:
chocolates
得到一个Observable
Observable
调用subscribe(onNext:)来监听其值变化。subscribe(onNext:)
接收的闭包在每次值变化时都会被执行。闭包中的入参就是Observable
变化后的最新值。除非你取消订阅,或者dispose订阅,你会一直收到变化通知。这个方法的返回的一个Disposable
(也是一个Observer
)。Observer
加入到我们的定义disposeBag
中。这会让被订阅对象被销毁时,订阅者也被处理。最后,删除updateCartButton()
方法的调用。
然后执行代码,你会看到巧克力列表:
但此时,点击单个巧克力,购物车位置一直显示的是“Item”。这是因为setupCartObserver()
没有被调用,导致Observer
并没有建立起来。在ChocolatesOfTheWorldViewController.swift的viewDidLoad()
方法最后调用该它。
再次编译运行,就会看到,当点击巧克力时,购物车自动更新了。
RxCocoa为原生UI组件添加了响应式API。这本例中,使用TableView的响应式API,可以不再用自己实现UITableViewDataSource
和UITableViewDelegate
。
实现步骤:
第一步:代码中删掉data source和delegate相关代码。
第二步是将table view使用到的数据从数组,改为一个Observable:
let europeanChocolates = Observable.just(Chocolate.ofEurope)
just(_:)
表明Observable
持有的值(value)是不变的。
注:对于不变化的值,是没有必要使用响应式编程的。所以在实际应用中,要避免拿着锤子看什么都是钉子,要实际分析一下你是否真的需要使用Rx。
第三步在//MARK: - Rx Setup
添加如下代码:
func setupCellConfiguration() { //1 europeanChocolates .bind(to: tableView .rx //2 .items(cellIdentifier: ChocolateCell.Identifier, cellType: ChocolateCell.self)) { //3 row, chocolate, cell in cell.configureWithChocolate(chocolate: chocolate) //4 } .disposed(by: disposeBag) //5}
代码释义:
bind(to:)
将europeanChocolates
这个Observable关联到table view每行所要执行的代码上items(cellIdentifier:cellType:)
传递相关参数。这样Rx框架根据这些信息实现一个wrapper data source,再设置给table view。再在viewDidLoad中调用下上面的setupCellConfiguration()方法。
这时运行程序,就会再次看到巧克力列表数据。
第四步来添加事件处理:
在//MARK: - Rx Setup位置添加如下代码
func setupCellTapHandling() { tableView .rx .modelSelected(Chocolate.self) //1 .subscribe(onNext: { [unowned self] chocolate in // 2 let newValue = ShoppingCart.sharedCart.chocolates.value + [chocolate] ShoppingCart.sharedCart.chocolates.accept(newValue) //3 if let selectedRowIndexPath = self.tableView.indexPathForSelectedRow { self.tableView.deselectRow(at: selectedRowIndexPath, animated: true) } //4 }) .disposed(by: disposeBag) //5}
代码释义:
modelSelected(_:)
传入Chocolate model类型,得到Observable对象subscribe(onNext:)
是给Observable对象注册事件处理,这个闭包代码会在一个model被选中时调用subscribe(onNext:)
返回的Disposable
加入到disposeBag
最后在viewDidLoad中调用setupCellTapHandling()
然后运行,就可以看到经过Rx API重写过的样例功能,和之前一样:点击列表中的巧克力,将他们添加到购物车中。
RxSwift也可以用来处理用户输入和表单验证。
非Reactive的做法是,实现UITextFieldDelegate
,然后实现很多if/else
来分别处理输入及其对应的动作。
Reactive Programming的做法则是更直接地将输入处理动作和逻辑绑定到input field上。
我们可以通过下面这个例子来具体体会一下。
先在BillingInfoViewController.swift中创建一个DisposeBag:
private let disposeBag = DisposeBag()
然后在//MARK: - Rx Setup
评论下的extension中添加如下代码:
func setupCardImageDisplay() { cardType .asObservable() //1 .subscribe(onNext: { [unowned self] cardType in self.creditCardImageView.image = cardType.image }) //2 .disposed(by: disposeBag) //3}
代码释义:
BehaviorRelay
的值添加一个Observable
Observable
来获得cardType
的变化disposeBag
上述代码就实现了一个Reactive的例子:当cardType变化时,creditCardImageView的图片会变成对应type定义的图片。接下来实现对文字变化的处理。
考虑到用户输入可能会很快,如果每次输入都执行验证代码可能会导致UI卡顿。所以我们需要实现一个throttle来控制,在throttle的时间间隔后才会再次验证输入,这样就可以避免对UI的阻塞。
RxSwift是支持Throttling的,因为有不少场景需要控制对变化的响应频次。下面我们来看看如何实现。
首先,在BillingInfoViewController中定义一个常量来表示throttle的间隔毫秒时间:
private let throttleIntervalInMilliseconds = 100
然后,在RX Setup
extension中加入下面的代码:
func setupTextChangeHandling() { let creditCardValid = creditCardNumberTextField .rx .text //1 .observeOn(MainScheduler.asyncInstance) .distinctUntilChanged() .throttle(.milliseconds(throttleIntervalInMilliseconds), scheduler: MainScheduler.instance) //2 .map { [unowned self] in self.validate(cardText: $0) //3 } creditCardValid .subscribe(onNext: { [unowned self] in self.creditCardNumberTextField.valid = $0 //4 }) .disposed(by: disposeBag) //5}
代码释义:
validate(cardText:)
,它可以将输入转换为creditCardValid的值,如果输入有效,creditCardValid的值会是true实现CVV的校验和上述思路一样。
先在setupTextChangeHandling()
底部添加如下代码:
let expirationValid = expirationDateTextField .rx .text .observeOn(MainScheduler.asyncInstance) .distinctUntilChanged() .throttle(.milliseconds(throttleIntervalInMilliseconds), scheduler: MainScheduler.instance) .map { [unowned self] in self.validate(expirationDateText: $0)} expirationValid .subscribe(onNext: { [unowned self] in self.expirationDateTextField.valid = $0 }) .disposed(by: disposeBag) let cvvValid = cvvTextField .rx .text .observeOn(MainScheduler.asyncInstance) .distinctUntilChanged() .map { [unowned self] in self.validate(cvvText: $0)} cvvValid .subscribe(onNext: { [unowned self] in self.cvvTextField.valid = $0 }) .disposed(by: disposeBag)
最后,将三个text field结合起来做验证,在setupTextChangeHandling()
加入以下代码:
let everythingValid = Observable .combineLatest(creditCardValid, expirationValid, cvvValid) { $0 && $1 && $2 //All must be true} everythingValid .bind(to: purchaseButton.rx.isEnabled) .disposed(by: disposeBag)
combineLatest(_:)将三个observable结合到一起再生成一个observable,everythingValid的值会是true或false。接着再讲everythingValid关联到purchaseButton的rx扩展属性isEnabled,以此来实现对该button是否可用状态的控制。
最后,在viewDidLoad中添加如下代码:
setupCardImageDisplay()setupTextChangeHandling()
运行代码,然后选择巧克力后,进入购物车:
然后点击checkout,进行支付界面:
这里可以看到,输入4,后面图标就显示了visa的图标。
接着,输入合法的CVV和有效期限,Buy Chocolate按钮就是可用状态了:
至此,我们就实现了一个简单的对用户输入的reactive方式的校验实现了。
领取专属 10元无门槛券
私享最新 技术干货