Bootstrap

RxSwift和RxCocoa入门

本文主要来自这篇文章

命令式编程 vs 响应式编程

维基百科上有一个更明确的例子:

对于命令式编程,当a = b + c, a由b+c计算得到;但是之后,b和c发生了变化,这个变化对a并不生效,a还是保持原有的值;而当使用响应式编程时,当b和c发生变化,程序不需要再次执行a = b + c公式,a的值就会自动更新。

响应式编程库现状

目前分为两派

  • ReactiveX主导的rx系列库,语言支持如RxSwift, RxKotlin, 平台支持如RxAndroid, RxCocoa(iOS);

  • ReactiveCocoa(GitHub发起)库,语言仅支持swift,objective-c,平台仅支持iOS。

相同点:

  • 都是Reactive Functional Programming(响应式函数编程)的实现

  • 二者github star数不分伯仲,均接近20k级别

  • 都有很好的社区支持

不同点:

  • Hot、Code Signal实现API不同:RAC设计了两个api分别对应hot、code signal,RxSwift仅有一个

  • 错误处理,RAC相对容易一些

  • UI Bindings,RAC有不少历史包袱,RxSwift则更容易使用

  • Rx系列支持更多语言和平台

关于这两个库的详细对比,见:

RxSwift和RxCocoa

RxSwift: 响应式编程在Swift语言领域的实现库

RxCocoa: 针对Cocoa平台(iOS & Mac OS)的响应式编程库

Observables and Observers

  • 一个Observable发送变化通知

  • 一个Observer订阅一个Observable,当Observable有变化时会被通知

多个Observer可以监听一个Observable,当发生变化时,所有Observer都会被通知

DisposeBag

DisposeBag是RxSwift提供的处理ARC和内存管理的工具。销毁一个父对象时,会使得DisposeBag中的Observer对象同时销毁。

当持有DisposeBag的对象的调用时,每个disposable Observer都会取消对监听对象的订阅,这时ARC就可以正常回收内存了。

如果没有DisposeBag的话,可能会出现两种情况,要么是observer会保留下来不被销毁,继续监听;要么被释放,造成崩溃。

建立Observer对象时,记得同时添加到DisposeBag中,来让DisposeBag帮助你回收该对象。

开始吧

先下载

打开编译后即可看到如下界面

这个例子功能很简单:选择巧克力后,会将其加入到购物车,点击右上角购物车图标可以进入购物车,然后进行结账和模拟支付。

非reactive的实现

ChocolatesOfTheWorldViewController.swift中可以看到实现和的extension。

在观察一下这个方法,它用来更新购物车中的巧克力数量,在下面两种情况时调用:

这就是命令式编程方式实现:你必须手动调用方法来更新购物车巧克力数量。

使用RxSwift改写购物车商品数量

购物车商品信息保存在这个单例中

定义在ShoppingCart.sharedCart中:

var chocolates: [Chocolate] = []

虽然可以给其定义中添加一个闭包,但问题是,这种做法只能在整个数组更新时才能被通知,而不是数组中任意元素变化就可以得到通知。

针对这种情况,RxSwift提供了解决方案,按照如下方式创建变量:

let chocolates: BehaviorRelay<[Chocolate]> = BehaviorRelay(value: [])

使用RxSwift的对象,持有一个Chocolate数组类型的值。这么做的目的是:通过对象的可以得到一个observable,这样我们就可以添加监听者来订阅对象的(chocolate数组)的变化。

上述做法的缺点是,对chocolate数组的修改必须修改为使用,这是为修改属性提供的方法。由于对数据的访问方式发生了变化,代码中相应的地方也要做修改。

ShoppingCart.swift

方法的修改:

return chocolates.reduce(0) {
// 修改为
return chocolates.value.reduce(0) {

方法的修改:

guard chocolates.count > 0 else {
// 修改为
guard chocolates.value.count > 0 else {

let setOfChocolates = Set(chocolates)
// 修改为
let setOfChocolates = Set(chocolates.value)

let count: Int = chocolates.reduce(0) {
// 修改为
let count: Int = chocolates.value.reduce(0) {

CartViewController.swift

方法的修改:

ShoppingCart.sharedCart.chocolates = []
// 修改为
ShoppingCart.sharedCart.chocolates.accept([])

ChocolatesOfTheWorldViewController.swift

方法的修改:

// 修改为
cartButton.title = "\(ShoppingCart.sharedCart.chocolates.value.count) \u{1f36b}"

方法的修改:

ShoppingCart.sharedCart.chocolates.append(chocolate)
// 修改为
let newValue =  ShoppingCart.sharedCart.chocolates.value + [chocolate]
ShoppingCart.sharedCart.chocolates.accept(newValue)

完成上述修改后,我们就可以来对添加observer了。

在ChocolatesOfTheWorldViewController.swift中,新增:

private let disposeBag = DisposeBag()

在的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大量使用函数链,就是说每一个函数接收前一个函数的结果。

对上述代码的解释:

最后,删除方法的调用。

然后执行代码,你会看到巧克力列表:

但此时,点击单个巧克力,购物车位置一直显示的是“Item”。这是因为没有被调用,导致并没有建立起来。在ChocolatesOfTheWorldViewController.swift的方法最后调用该它。

再次编译运行,就会看到,当点击巧克力时,购物车自动更新了。

使用RxCocoa对TableView进行Reactive改造

RxCocoa为原生UI组件添加了响应式API。这本例中,使用TableView的响应式API,可以不再用自己实现和。

实现步骤:

第一步:代码中删掉data source和delegate相关代码。

第二步是将table view使用到的数据从数组,改为一个Observable:

let europeanChocolates = Observable.just(Chocolate.ofEurope)

表明持有的值(value)是不变的。

注:对于不变化的值,是没有必要使用响应式编程的。所以在实际应用中,要避免拿着锤子看什么都是钉子,要实际分析一下你是否真的需要使用Rx。

第三步在添加如下代码:

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
}

代码释义:

再在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
}

代码释义:

最后在viewDidLoad中调用

然后运行,就可以看到经过Rx API重写过的样例功能,和之前一样:点击列表中的巧克力,将他们添加到购物车中。

使用RxSwift处理用户输入(Direct Text Input)

RxSwift也可以用来处理用户输入和表单验证。

非Reactive的做法是,实现,然后实现很多来分别处理输入及其对应的动作。

Reactive Programming的做法则是更直接地将输入处理动作和逻辑绑定到input field上。

我们可以通过下面这个例子来具体体会一下。

先在BillingInfoViewController.swift中创建一个DisposeBag:

private let disposeBag = DisposeBag()

然后在评论下的extension中添加如下代码:

func setupCardImageDisplay() {
  cardType
    .asObservable() //1
    .subscribe(onNext: { [unowned self] cardType in
      self.creditCardImageView.image = cardType.image
    }) //2
    .disposed(by: disposeBag) //3
}

代码释义:

上述代码就实现了一个Reactive的例子:当cardType变化时,creditCardImageView的图片会变成对应type定义的图片。接下来实现对文字变化的处理。

考虑到用户输入可能会很快,如果每次输入都执行验证代码可能会导致UI卡顿。所以我们需要实现一个throttle来控制,在throttle的时间间隔后才会再次验证输入,这样就可以避免对UI的阻塞。

RxSwift是支持Throttling的,因为有不少场景需要控制对变化的响应频次。下面我们来看看如何实现。

首先,在BillingInfoViewController中定义一个常量来表示throttle的间隔毫秒时间:

private let throttleIntervalInMilliseconds = 100

然后,在 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
}

代码释义:

实现CVV的校验和上述思路一样。

先在底部添加如下代码:

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结合起来做验证,在加入以下代码:

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方式进行校验的示例。