Chapter 5: Filtering Operators

学习一项新技术堆栈有点像建造摩天大楼。在你能到达天空之前,你必须建立一个坚实的基础。到目前为止,您已经建立了对RxSwift的基本理解,现在是时候开始建立您的知识库和技能集了,一次一个水平。

这一章将告诉你RxSwift的过滤操作符,你可以用它来对发出的事件应用条件约束,这样订阅者就只接收到它想要处理的元素。如果你曾经在Swift标准库中使用过filter(_:)方法,那么你已经成功了一半。如果没有,没有烦恼;在这一章结束时,你将成为这方面的专家。

Getting started

本章的初始项目名为RxPlayground。在project文件夹中运行./bootstrap.sh后,Xcode会打开。在项目导航器中选择RxSwiftPlayground,你就可以开始操作了。

Ignoring operators

您将直接进入并查看RxSwift中一些有用的过滤操作符,从ignoreElements开始。如下面的弹珠图所示,ignoreElements将忽略所有接下来的事件。但是,它将允许停止事件通过,例如已完成或错误事件。

允许停止事件通过通常是在所有弹珠图中隐含的。这次它被显式地调用了,因为所有的ignoreElements都会让它通过。

avatar

注意:到目前为止,您已经看到了用于类型的弹珠图。这种弹珠图的形式可以帮助您可视化操作人员是如何工作的。最上面一行是被订阅的可观察对象。框表示操作符及其参数,底线是订阅者,或者更具体地说,订阅者在操作符完成其工作后将接收到的内容。

要查看ignoreElements的实际作用,请将这个例子添加到你的游乐场:

example(of: "ignoreElements") {
  // 1
  let strikes = PublishSubject<String>()

  let disposeBag = DisposeBag()

  // 2
  strikes
    .ignoreElements()
    .subscribe { _ in
      print("You're out!")
    }
    .disposed(by: disposeBag)
}

你是这样做的:

  1. Create a strikes subject.
  2. Subscribe to all strikes’ events, but ignore all next events by using ignoreElements.

注意:如果您不太了解击球、击球手和棒球游戏,那么当您决定从编程中休息一下时,您可以阅读以下内容:https://simple.wikipedia.org/wiki/Baseball。

如果你只希望在可观察对象结束时(通过完成或错误事件)得到通知,那么ignoreElements操作符非常有用。将以下代码添加到示例中:

strikes.onNext("X")
strikes.onNext("X")
strikes.onNext("X")

尽管这个击球手似乎不能击中谷仓的大侧面,显然已经出局了,但什么也没打印出来,因为你忽略了接下来的所有事件。您可以自行向该主题添加一个已完成的事件,以便通知订阅者。添加以下代码:

strikes.onCompleted()

现在订阅者将收到已完成的事件,并打印出任何击球手都不想听到的口号:

--- Example of: ignoreElements ---
You're out!

善于发现的读者可能会注意到ignoreElements实际上返回一个Completable,这很有意义,因为它只会发出一个完成或错误事件。

有时候,你可能只想处理由可观察对象发出的第n(序数)个元素,比如第三次攻击。为此,您可以使用elementAt,它接受您想要接收的元素的索引,并忽略其他一切。在弹珠图中,elementAt被传递了索引1,所以它只允许通过第二个元素。

avatar

添加这个新示例:

example(of: "elementAt") {

  // 1
  let strikes = PublishSubject<String>()

  let disposeBag = DisposeBag()

  //  2
  strikes
    .elementAt(2)
    .subscribe(onNext: { _ in
      print("You're out!")
    })
    .disposed(by: disposeBag)
}

比赛详情:

  1. You create a subject.
  2. You subscribe to the next events, ignoring all but the 3rd next event, found at index 2.

现在您可以简单地在主题中添加新的打击,您的订阅会让您知道击球手何时三振出局。添加此代码:

strikes.onNext("X")
strikes.onNext("X")
strikes.onNext("X")

“Hey batta, batta, batta — swing batta!”

--- Example of: elementAt ---
You're out!

关于元素(at:)的一个有趣事实是:一旦元素在提供的索引处发出,订阅就终止。

ignoreElements和elementAt过滤由可观察对象发出的元素。当您的过滤需要超过全部或一个时,请使用filter作符。它接受一个谓词闭包并将其应用于发出的每个元素,只允许通过谓词解析为true的元素。

查看这个弹珠图,其中只有1和2允许通过,因为过滤器的谓词只允许小于3的元素。

avatar

将这个例子添加到你的操场:

example(of: "filter") {
  let disposeBag = DisposeBag()

  // 1
  Observable.of(1, 2, 3, 4, 5, 6)
    // 2
    .filter { $0.isMultiple(of: 2) }
    // 3
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)
}

从头再来:

  1. You create an observable of some predefined integers.
  2. You use the filter operator to apply a conditional constraint to prevent odd numbers from getting through.
  3. You subscribe and print out the elements that pass the filter predicate.

应用这个过滤器的结果是只打印偶数:

--- Example of: filter ---
2
4
6

Skipping operators

如果想要跳过特定数量的元素,请使用skip操作符。它允许您忽略前n个元素,其中n是作为参数传递的数字。这个弹珠图显示skip被传递为2,因此它忽略了前2个元素。

avatar

将这个新例子添加到你的操场:

example(of: "skip") {
  let disposeBag = DisposeBag()

  // 1
  Observable.of("A", "B", "C", "D", "E", "F")
    // 2
    .skip(3)
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)
}

有了这个代码,你:

  1. Create an observable of letters.
  2. Use skip to skip the first 3 elements and subscribe to next events.

跳过前3个元素后,只打印D、E和F:

--- Example of: skip ---
D
E
F

skip操作符有一小家族。与filter类似,skipWhile允许包含一个谓词来确定跳过了什么。但是,与筛选订阅生命周期元素的filter不同,skipWhile只会跳过某些内容,然后让其他内容从该点开始通过。

对于skipWhile,返回true将导致跳过元素,返回false将允许它通过。它是filter的反义词。

在这个弹球图中,1被阻止,因为1% % 2等于1,但是2被允许通过,因为它失败了谓词,而3(以及其他所有内容)通过了,因为skipWhile不再跳过。

avatar

将这个新例子添加到你的操场:

example(of: "skipWhile") {
  let disposeBag = DisposeBag()

  // 1
  Observable.of(2, 2, 3, 4, 4)
    // 2
    .skipWhile { $0.isMultiple(of: 2) }
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)
}

Here’s what you did:

  1. Create an observable of integers.
  2. Use skipWhile with a predicate that skips elements until an odd integer is emitted.

记住,skip只会跳过元素,直到第一个元素被允许通过,然后剩下的所有元素都被允许通过。这个例子输出:

--- Example of: skipWhile ---
3
4
4

例如,如果您正在开发一个保险索赔应用程序,您可以使用skipWhile来拒绝承保,直到满足免赔额。

到目前为止,您已经根据静态条件进行了过滤。如果你想根据另一个可观察对象动态地过滤元素呢?有几个操作符可供选择。

第一个是skipUntil,它将继续跳过源可观察对象(你正在订阅的对象)中的元素,直到其他触发可观察对象发出。在这个弹珠图中,skipUntil忽略顶部一行的源可观察对象发出的元素,直到第二行的触发器可观察对象发出下一个事件。然后它会停止skipping,从那一刻开始让所有的东西都通过。

avatar

添加这个例子来看看skipUntil是如何在代码中工作的:

example(of: "skipUntil") {
  let disposeBag = DisposeBag()

  // 1
  let subject = PublishSubject<String>()
  let trigger = PublishSubject<String>()

  // 2
  subject
    .skipUntil(trigger)
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)
}

In this code, you:

  1. Create a subject to model the data you want to work with, and another subject to act as a trigger.
  2. Use skipUntil and pass the trigger subject. When trigger emits, skipUntil stops skipping.

在主题上添加几个接下来的事件:

subject.onNext("A")
subject.onNext("B")

没有打印,因为你跳过了。现在在触发器上添加一个新的next事件:

trigger.onNext("X")

这将导致skipUntil停止跳过。从这一点开始,所有的元素都可以通过。添加下一个事件到主题:

subject.onNext("C")

果然,它被打印出来了:

--- Example of: skipUntil ---
C

Taking operators

Taking是skipping的反义词。当你想获取元素时,RxSwift可以满足你的需求。您将学习的第一个取操作符是take,正如此弹珠图所描述的,它将取指定的元素数中的第一个。

avatar

将这个示例添加到您的游乐场中,以探索第一个take操作符:

example(of: "take") {
  let disposeBag = DisposeBag()

  // 1
  Observable.of(1, 2, 3, 4, 5, 6)
    // 2
    .take(3)
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)
}

With this code, you:

  1. Create an observable of integers.
  2. Take the first 3 elements using take.

What you take is what you get. The output is:

--- Example of: take ---
1
2
3

takeWhile操作符的工作原理与skipWhile类似,只是您使用的是taking而不是skipping。

avatar

此外,如果希望引用要发出的元素的索引,可以使用enumerated操作符。它生成包含可观察对象中每个被触发元素的索引和元素的元组,类似于Swift标准库中的enumerated方法的工作方式。

在你的操场上输入这个新例子:

example(of: "takeWhile") {
  let disposeBag = DisposeBag()

  // 1
  Observable.of(2, 2, 4, 4, 6, 6)
    // 2
    .enumerated()
    // 3
    .takeWhile { index, integer in
      // 4
      integer.isMultiple(of: 2) && index < 3
    }
    // 5
    .map(\.element)
    // 6
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)
}

From the top, you:

  1. Create an observable of integers.
  2. Use the enumerated operator to get tuples containing the index and value of each element emitted.
  3. Use the takeWhile operator, and destructure the tuple into individual arguments.
  4. Pass a predicate that will take elements until the condition fails.
  5. Use map — which works just like the Swift Standard Library map — to reach into the tuple returned from takeWhile and get the element.
  6. Subscribe to and print out next elements.

结果是,您只接收到整数为偶数的元素,直到元素的索引为3或更大。

--- Example of: takeWhile ---
2
2
4

与takeWhile相反,有一个takeUntil操作符,该操作符接受元素,直到满足谓词。它的第一个参数还接受一个behavior参数,该参数指定您是希望包含还是排除与谓词匹配的最后一个元素。

avatar

将这个新例子添加到你的操场,看看它是如何工作的:

example(of: "takeUntil") {
  let disposeBag = DisposeBag()

  // 1
  Observable.of(1, 2, 3, 4, 5)
    // 2
    .takeUntil(.inclusive) { $0.isMultiple(of: 4) }
    .subscribe(onNext: {
      print($0)
    })
  .disposed(by: disposeBag)
}

With this code, you:

  1. Create an Observable of sequential integers.
  2. Use the takeUntil operator with inclusive behavior.

这段代码输出传递谓词的元素之前的元素,并包括这些元素:

--- Example of: takeUntil ---
1
2
3
4

现在,将行为从.包容性(.inclusive )改为.排他性(.exclusive),再次运行游乐场。这一次,传递谓词的元素被排除了:

--- Example of: takeUntil ---
1
2
3

和skipUntil一样,还有一个变种的takeUntil也适用于触发器可观察对象。下面的弹珠图显示了takeUntil从源可观察对象中提取,直到触发可观察对象发出一个元素。

avatar

添加这个新示例,就像前面创建的skipUntil示例一样:

example(of: "takeUntil trigger") {
  let disposeBag = DisposeBag()

  // 1
  let subject = PublishSubject<String>()
  let trigger = PublishSubject<String>()

  // 2
  subject
    .takeUntil(trigger)
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)

  // 3
  subject.onNext("1")
  subject.onNext("2")
}

Here’s what you did:

  1. Create a primary subject and a trigger subject.
  2. Use takeUntil, passing the trigger that will cause takeUntil to stop taking once it emits.
  3. Add a couple of elements onto subject.

元素会被打印出来,因为takeUntil处于taking模式:

--- Example of: takeUntil trigger ---
1
2

现在在触发器上添加一个元素,然后在主题上添加另一个元素:

trigger.onNext("X")

subject.onNext("3")

X停止抓取,所以3不允许通过,没有更多的打印。

有一种方法可以使用takeUntil和RxCocoa库中的API来释放订阅,而不是将其添加到一个释放包中。你将在第三节学习RxCocoa,“iOS Apps with RxCocoa”一般来说,避免内存泄漏的安全方法是总是将订阅添加到一个释放包中。然而,为了完整起见,这里有一个你如何在RxCocoa中使用takeUntil的例子——不要把这个带入你的操场,因为它无法编译:

_ = someObservable
    .takeUntil(self.rx.deallocated)
    .subscribe(onNext: {
        print($0)
    })

在上面的代码中,self的释放是导致takeUntil停止taking的触发器,self通常是一个视图控制器或视图模型。

Distinct operators

接下来的两个操作符可以防止重复的连续项通过。如图所示,distinctUntilChanged只能防止相邻的重复项,所以第二个1可以通过。

avatar

将这个新例子添加到你的操场:

example(of: "distinctUntilChanged") {
  let disposeBag = DisposeBag()

  // 1
  Observable.of("A", "A", "B", "B", "A")
    // 2
    .distinctUntilChanged()
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)
}

What you do with this code:

  1. Create an observable of letters.
  2. Use distinctUntilChanged to prevent sequential duplicates from getting through.

distinctUntilChanged操作符只能防止连续的重复,所以第二个A和第二个B会被阻止,因为它们与前一个元素相等。但是,第三个A被允许通过,因为它不等于它的前一个元素。打印结果如下:

--- Example of: distinctUntilChanged ---
A
B
A

这些是String的实例,它们符合Equatable。但是,你可以选择使用distinctUntilChanged(_:)来提供你自己的自定义逻辑来测试是否相等;传递的参数是比较器。

在下面的弹珠图中,将比较具有名为value属性的对象是否基于value进行相等性。

avatar

将这个稍微复杂一点的例子添加到你的操场上:

example(of: "distinctUntilChanged(_:)") {
 let disposeBag = DisposeBag()
 
 // 1
 let formatter = NumberFormatter()
 formatter.numberStyle = .spellOut

 // 2
 Observable<NSNumber>.of(10, 110, 20, 200, 210, 310)
   // 3
   .distinctUntilChanged { a, b in
     // 4
     guard
       let aWords = formatter
         .string(from: a)?
         .components(separatedBy: " "),
       let bWords = formatter
         .string(from: b)?
         .components(separatedBy: " ")
       else {
         return false
     }

     var containsMatch = false

     // 5
     for aWord in aWords where bWords.contains(aWord) {
       containsMatch = true
       break
     }

     return containsMatch
   }
   // 6
   .subscribe(onNext: {
     print($0)
   })
   .disposed(by: disposeBag)
}

From the top, you:

  1. 创建一个数字格式化程序来拼写出每个数字。
  2. 创建一个NSNumbers的可观察对象,而不是int,这样你就不必在接下来使用formatter时转换整数。
  3. 使用distinctUntilChanged(_😃,它接受一个接收每个序列元素对的谓词闭包。
  4. 使用guard有条件地绑定由一个空格分隔的元素组件,否则返回false。
  5. 迭代第一个数组中的每个单词,看看它是否包含在第二个数组中。
  6. 根据您提供的比较逻辑订阅并打印出被认为是不同的元素。

因此,只打印不同的整数,考虑到在每对整数中,一个不包含另一个的任何单词组成部分。

--- Example of: distinctUntilChanged(_:) ---
10
20
200

distinctUntilChanged(_:)操作符在你想清楚地防止不符合Equatable类型的重复时也很有用。

代码地址