1. Chapter 2: Observables

现在您已经学习了RxSwift的一些基本概念,是时候开始使用可观察对象了。

在本章中,你将学习几个创建和订阅可观察对象的例子。一些可观察对象在现实世界中的使用可能看起来有点晦涩,但请放心,您将获得重要的技能,并了解RxSwift中可用的可观察对象的类型。你将在本书的其余部分使用这些技巧——甚至更多!

1.1. Getting started

在本章中,你将使用一个Xcode项目,它已经包含了一个playground和RxSwift框架。要开始,打开终端应用程序,导航到本章的starter文件夹,然后到RxPlayground项目文件夹。最后,输入以下命令运行bootstrap.sh脚本:

./bootstraph.sh

引导过程将花费几秒钟;请记住,每当您想要打开这个playground项目时,您将需要重复上述步骤。您不能直接打开playground文件或工作区。

通过Project导航器中的Sources文件夹,向下扭曲playground页面,并选择SupportCode.swift。它包含以下助手函数example(of:):

public func example(of description: String, action: () -> Void) {
  print("\n--- Example of:", description, "---")
  action()
}

在完成本章的过程中,您将使用这个函数来封装不同的示例。稍后您将看到如何使用这个函数。

但在你深入这个问题之前,现在可能是回答这个问题的好时机:什么是observable?

1.2. What is an observable?

***Observables***是Rx的核心。您将花一些时间讨论什么是可观察对象,如何创建它们,以及如何使用它们。

你会看到“observable”、“observable sequence”和“sequence”在Rx中可以互换使用。实际上,它们都是一样的。您甚至可能会不时看到一个偶然的“Stream”,特别是来自不同的响应式编程环境的开发人员来到RxSwift。“Stream”也指同样的事情,但在RxSwift中,所有很酷的孩子都称它为序列,而不是流。在RxSwift…

或者是有序列的东西。 Observable 就是一个序列,有一些特殊powers。其中一个——实际上是最重要的一个——就是它是异步的。Observable 在一段时间内产生事件,这被称为emitting。 事件可以包含值,如数字或自定义类型的实例,也可以是可识别的手势,如点击。

将其概念化的最佳方法之一是使用弹珠图,即绘制在时间轴上的值。

avatar

从左到右的箭头表示时间,编号的圆圈表示序列的元素。元素1会被触发,一段时间后,2和3会被触发。你问,还有多少时间?它可以出现在可观察对象生命周期中的任何一个点上——这就引出了你将要学习的下一个主题:可观察对象的生命周期。

1.3. Lifecycle of an observable

在前面的弹珠图中,observable 发射出三个元素。当一个observable发出一个元素时,它会在所谓的next事件中这样做。

这是另一个弹珠图,这次包括一个竖条,代表这个可观察到的路的尽头。

avatar

这个可观察对象发出三个点击事件,然后它结束。这叫做一个completed事件,也就是说,它被终止了。例如,或许这些taps是在一个视图上被驳回的。重要的是,可观察对象被终止了,不能再发出任何东西。这是正常的终止。然而,有时事情也会出错。

avatar

在这个弹珠图中出现了一个错误,用红色的x表示。observable 发出了一个包含错误的错误事件。这与一个可观察对象以一个完成的事件正常结束时是一样的。如果一个可观察对象发出了一个error事件,它也会被终止,并且不能再发出任何其他事件。

这里是一个简短的回顾:

  • 一个observable发出包含元素的next事件。
  • 它可以继续执行此操作,直到触发一个terminating event,即一个an error or completed event。
  • 一旦可观察对象被终止,它就不能再发出事件。

事件表示为枚举案例。下面是RxSwift源代码中的实际实现:

/// Represents a sequence event.
///
/// Sequence grammar: 
/// **next\* (error | completed)**
public enum Event<Element> {
    /// Next element is produced.
    case next(Element)

    /// Sequence terminated with an error.
    case error(Swift.Error)

    /// Sequence completed successfully.
    case completed
}

在这里,您可以看到next事件包含了一些Element的实例,错误事件包含了一个Swift.Error的实例和completed 事件只是不包含任何数据的停止事件。

既然你理解了observable是什么,它做了什么, 您将创建一些observable来查看它们的运行情况。

1.4. Creating observables

从当前文件切换回RxSwiftPlayground,并添加以下代码:

example(of: "just, of, from") {
  // 1
  let one = 1
  let two = 2
  let three = 3
  
  // 2
  let observable = Observable<Int>.just(one)
}

在上述代码中,您:

  1. 定义将在下面的示例中使用的整数常量。

  2. 使用just方法和一个整型常量创建一个Int类型的可观察序列。

just方法的名字很贴切,因为它所做的只是创建一个只包含单个元素的可观察序列。它是Observable上的一个静态方法。然而,在Rx中,方法被称为“operators”。 眼尖的人应该能猜出你接下来要去哪个。

将此代码添加到相同的示例中:

let observable2 = Observable.of(one, two, three)

这一次,您没有显式声明类型。你可能会认为,因为你给了它几个整数,所以该类型是[Int]可观察对象。

但是,选择单击observable2来显示它的推断类型,你会发现它是Int类型的可观察对象,而不是数组:

avatar

这是因为of操作符有一个可变参数,Swift可以根据它推断Observable的类型。

avatar

当你想要创建一个可观察数组时,传递一个数组给of。将以下代码添加到示例的底部:

let observable3 = Observable.of([one, two, three])

点击observable3,你会发现它确实是一个[Int]的Observable。just操作符也可以将一个数组作为其单个元素,这乍一看可能有点奇怪。但是,数组是单个元素,而不是它的内容。

另一个可以用来创建可观察对象的操作符是from。将以下代码添加到示例的底部:

let observable4 = Observable.from([one, two, three])

from操作符从一个类型化元素数组中创建单个元素的可观察对象。点击Observable 4,你会看到它是Int类型的可观察对象,而不是[Int]。from操作符只接受一个数组。

此时你的主机可能看起来很空空如也。这是因为除了示例头文件外,您没有打印任何内容。是时候通过订阅可观察对象来改变这种情况了。

1.5. Subscribing to observables

从你作为iOS开发者的经验来看,你可能对NotificationCenter很熟悉;它向观察者广播通知。然而,这些被观察到的通知不同于RxSwift observable。下面是一个UIKeyboardDidChangeFrame通知的观察者的例子,它的处理程序作为一个尾随的闭包(不要把这段代码添加到你的playground上):

let observer = NotificationCenter.default.addObserver(
  forName: UIResponder.keyboardDidChangeFrameNotification,
  object: nil,
  queue: nil) { notification in
  // Handle receiving notification
}

订阅RxSwift可观察对象是非常相似的;你把观察称为订阅一个可观察对象。所以使用subscribe()代替addObserver()。不同于NotificationCenter,开发者通常只使用它的.default单例实例,Rx中的每个可观察对象都是不同的。

更重要的是,在拥有订阅方之前,可观察对象不会发送事件或执行任何工作

记住,一个可观察对象实际上是一个序列定义,而订阅一个可观察对象实际上更像是在Swift标准库中的迭代器上调用next()(不要把这段代码添加到你的playground):

let sequence = 0..<3

var iterator = sequence.makeIterator()

while let n = iterator.next() {
  print(n)
}

/* Prints:
 0
 1
 2
 */

订阅可观察对象更加简化。你也可以为每个可观察对象可以触发的事件类型添加处理程序。回想一下,一个可观察对象会发出next、error和completed事件。next事件将发送的元素传递给处理程序,错误事件包含一个错误实例。

要查看实际操作,请将这个新示例添加到您的游乐场中。记住要在前一个示例的右花括号之后单独插入每个新示例。

example(of: "subscribe") {
  let one = 1
  let two = 2
  let three = 3
  
  let observable = Observable.of(one, two, three)
}

这与前面的示例类似,只是这次使用的是of运算符。现在在这个例子的底部添加下面的代码,来订阅这个可观察对象:

observable.subscribe { event in
  print(event)
}

选择单击subscribe操作符,观察它接受一个闭包参数,该闭包参数接收Int类型的Event,不返回任何东西,而subscribe返回一个Disposable。你很快就会了解到disposables。

avatar

这个订阅将打印出observable发出的每个事件:

--- Example of: subscribe ---
next(1)
next(2)
next(3)
completed

这个可观察对象为每个元素触发一个next事件,然后触发一个completed事件并被终止。在处理可观察对象时,您通常主要感兴趣的是next事件发出的elements,而不是事件本身。

要查看一种直接访问元素的方法,用下面的代码替换上面的订阅代码:

observable.subscribe { event in
  if let element = event.element {
    print(element)
  }
}

Event有一个元素属性。这是一个可选值,因为只有next事件才有一个元素。因此,如果元素不是nil,可以使用可选绑定来打开它。现在只打印元素,而不是包含元素的事件,也不是已完成的事件:

1
2
3

这是一个很好的模式,它被如此频繁地使用,以至于在RxSwift中有一个它的快捷方式。对于observable发出的每种类型的事件,都有一个订阅操作符:next、error和completed。

用以下代码替换之前的订阅代码:

observable.subscribe(onNext: { element in
  print(element)
})

现在,您只处理下一个事件元素,而忽略其他所有内容。onNext闭包接收next event的元素作为参数,所以您不必像以前那样从事件中手动提取它。

现在您知道了如何创建可观察的一个元素和多个元素。但是零元素的可观测值呢? empty操作符创建一个空的可观察序列,其中没有元素; 它将只发出一个completed的事件。

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

example(of: "empty") {
  let observable = Observable<Void>.empty()
}

如果一个可观察对象不能被推断,那么它必须被定义为一个特定的类型。因此,必须显式定义类型,因为empty没有任何推断类型的依据。通常使用Void是因为不会发出任何东西。

将以下代码添加到订阅空可观察对象的示例中:

observable.subscribe(
  // 1
  onNext: { element in
    print(element)
  },
  
  // 2
  onCompleted: {
    print("Completed")
  }
)

在上述代码中,您:

  1. 处理next事件,就像在前面的示例中所做的那样。

  2. 只需打印一条消息,因为.completed事件不包含元素。

在控制台中,你会看到empty只会发出一个.completed事件:

--- Example of: empty ---
Completed

empty observable 有什么用?当你想要返回一个立即终止或有意为零的可观察对象时,它们很方便。

与empty操作符相反,never操作符创建的可观察对象不会产生任何内容,也不会终止。它可以用来表示无限的持续时间。将这个例子添加到你的操场

example(of: "never") {
  let observable = Observable<Void>.never()
  
  observable.subscribe(
    onNext: { element in
      print(element)
    },
    onCompleted: {
      print("Completed")
    }
  )
}

除了示例头外,不会打印任何内容。即使是“Completed”。你怎么知道这是否有效?在阅读挑战部分之前,保持好奇心。

到目前为止,您已经使用了特定元素或值的可观察对象。然而,也可以从一系列的值中生成一个可观察对象。

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

example(of: "range") {
  // 1
  let observable = Observable<Int>.range(start: 1, count: 10)
  
  observable
    .subscribe(onNext: { i in  
      // 2
      let n = Double(i)
      
      let fibonacci = Int(
        ((pow(1.61803, n) - pow(0.61803, n)) /
          2.23606).rounded()
      )
      
      print(fibonacci)
  })
}
  1. 使用range操作符创建一个可观察对象,该操作符接受一个起始整数值和一个要生成的连续整数的计数。

  2. 计算并打印每个发射的元素的第n个Fibonacci数。

实际上有一个比onNext处理程序更好的位置来放置转换所发出元素的代码。你将在第7章“Transforming Operators”中了解到这一点。

除了never()示例之外,到目前为止,您已经使用了自动触发完成事件并自然终止的可观察对象。这样做可以让你专注于创建和订阅可观察对象的机制,但这忽略了订阅可观察对象的一个重要方面。在继续之前,是时候做些家务了。

1.6. Disposing and terminating

记住,在接收到订阅之前,可观察对象不会做任何事情。是订阅触发了可观察对象的工作,导致它发出新的事件,直到一个错误或已完成的事件终止了可观察对象。但是,你也可以通过取消订阅来手动终止一个可观察对象。

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

example(of: "dispose") {
  // 1
  let observable = Observable.of("A", "B", "C")
  
  // 2
  let subscription = observable.subscribe { event in
    // 3
    print(event)
  }
}

简单来说

  1. 创建一个可观察的字符串。

  2. 订阅可观察对象,这次将返回的Disposable保存为一个名为subscription的局部常量。

  3. 在处理程序中打印每个发出的事件。

要显式取消订阅,请调用dispose()。在你取消订阅或者释放订阅之后,当前示例中的可观察对象将停止发出事件。

将以下代码添加到示例的底部:

subscription.dispose()

单独管理每个订阅会很繁琐,所以RxSwift包含了一个DisposeBag类型。处置包保存一次性物品——通常使用dispose(by:)方法添加——并在处置包即将被释放时对每个一次性物品调用dispose()。

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

example(of: "DisposeBag") {
  // 1
  let disposeBag = DisposeBag()
  
  // 2
  Observable.of("A", "B", "C")
    .subscribe { // 3
      print($0)
    }
    .disposed(by: disposeBag) // 4
}

一步一步:

  1. 创建一个释放包。

  2. 创建一个observable。

  3. 订阅这个observable并使用默认参数$0打印出发出的事件。

  4. 将订阅返回的Disposable添加到dispose bag。

这是你最常使用的模式:创建并订阅一个可观察对象,然后立即将订阅添加到一个dispose bag。

为什么要用disposables呢?

如果你忘记向dispose包添加订阅,或者在订阅完成后手动调用dispose,或者以其他方式导致observable在某个时刻终止,你可能会泄漏内存。

忘了也别担心;Swift编译器应该警告你关于未使用的disposables。

在前面的例子中,您创建了具有特定next事件元素的可观察对象。使用create操作符是指定可观察对象将发出给订阅者的所有事件的另一种方法。

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

example(of: "create") {
  let disposeBag = DisposeBag()
  
  Observable<String>.create { observer in
    
  }
}

create操作符接受一个名为subscribe的参数。它的工作是提供对可观察对象调用subscribe的实现。换句话说,它定义了将发送给订阅者的所有事件。

如果您现在选择单击create,您将不会获得Quick Help文档,因为此代码尚未编译。下面是预览:

avatar

ubscribe参数是一个转义闭包,它接受一个AnyObserver并返回一个Disposable。AnyObserver是一种泛型类型,它可以方便地将值添加到可观察序列中,然后将其发送给订阅者。

将create的实现更改为如下:

Observable<String>.create { observer in
  // 1
  observer.onNext("1")
  
  // 2
  observer.onCompleted()
  
  // 3
  observer.onNext("?")
  
  // 4
  return Disposables.create()
}

下面是你对这段代码做的事情:

  1. 在观察者上添加下一个事件。onNext(:)是on(.next(😃)的一个方便方法。

  2. 将一个完成的事件添加到观察者。类似地,onCompleted是on(.completed)的一个方便方法

  3. 在观察者上添加另一个下一个事件。

  4. 返回一个disposable对象,定义了当你的可观察对象被终止或销毁时发生什么;在这种情况下,不需要进行任何清理,因此返回一个空的disposable。

认为第二个onNext元素(?)可以被发送到订阅者吗?为什么或为什么不?

要看你是否猜对了,在create实现后的下一行添加以下代码来订阅可观察对象:

.subscribe(
  onNext: { print($0) },
  onError: { print($0) },
  onCompleted: { print("Completed") },
  onDisposed: { print("Disposed") }
)
.disposed(by: disposeBag)

你订阅了这个可观察对象,并分别使用传递给onNext和onError处理程序的元素和错误参数的默认参数名实现了所有的处理程序。结果是,输出第一个下一个事件元素“Completed”和“dispose”。第二个next事件不会被打印,因为这个可观察对象发出了一个已完成的事件,并在它被添加之前终止。

--- Example of: create ---
1
Completed
Disposed

如果你给观察者添加一个错误会发生什么?在示例的顶部添加以下代码,以定义一个单一情况的错误类型:

enum MyError: Error {
  case anError
}

接下来,在观察者之间添加以下代码行。onNext和observer.onCompleted 调用:

observer.onError(MyError.anError)

现在可观察对象会发出错误并终止:

--- Example of: create ---
1
anError
Disposed

如果没有添加已完成或错误事件,也没有向disposeBag添加订阅,会发生什么?注释掉observer.onError,observer.onCompleted和disposed(by: disposeBag) 代码行来查找。

以下是完整的实现:

example(of: "create") {
  enum MyError: Error {
    case anError
  }
  
  let disposeBag = DisposeBag()
  
  Observable<String>.create { observer in
    // 1
    observer.onNext("1")
    
//    observer.onError(MyError.anError)
    
    // 2
//    observer.onCompleted()
    
    // 3
    observer.onNext("?")
    
    // 4
    return Disposables.create()
  }
  .subscribe(
    onNext: { print($0) },
    onError: { print($0) },
    onCompleted: { print("Completed") },
    onDisposed: { print("Disposed") }
  )
//  .disposed(by: disposeBag)
}

祝贺您,您刚刚泄漏了内存!可观察对象永远不会结束,而一次性对象也永远不会被丢弃。

--- Example of: create ---
1
?

如果您不能忍受让这个示例处于泄漏状态,请随意取消对添加已完成事件的行或向disposeBag添加订阅的代码的注释。

1.7. Creating observable factories

与其创建一个等待订阅者的可观察对象,还不如创建可观察工厂,将一个新的可观察对象出售给每个订阅者。

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

example(of: "deferred") {
  let disposeBag = DisposeBag()
  
  // 1
  var flip = false
  
  // 2
  let factory: Observable<Int> = Observable.deferred {
    
    // 3
    flip.toggle()
    
    // 4
    if flip {
      return Observable.of(1, 2, 3)
    } else {
      return Observable.of(4, 5, 6)
    }
  }
}

从上面开始,你:

  1. 创建一个Bool标志来翻转要返回的可观察对象。

  2. 使用递延操作符创建Int工厂的可观察对象。

  3. 切换翻转,每次订阅工厂时都会发生。

  4. 根据flip是真还是假返回不同的可观察对象。

从外部看,一个可观察工厂与一个常规的可观察对象是难以区分的。将此代码添加到示例的底部,以订阅工厂四次:

for _ in 0...3 {
  factory.subscribe(onNext: {
    print($0, terminator: "")
  })
  .disposed(by: disposeBag)

  print()
}

每次你订阅factory,你会得到相反的观察结果。换句话说,你得到123,然后是456,每次创建一个新的订阅时,这个模式都会重复:

--- Example of: deferred ---
123
456
123
456

1.8. Using Traits

特征是具有比常规可观察对象更窄的一系列行为的可观察对象。它们的使用是可选的;你可以在任何可能使用trait的地方使用一个常规的可观察对象。它们的目的是提供一种更清楚地向代码读者或API消费者传达意图的方式。使用trait所隐含的上下文可以帮助您的代码更加直观。

RxSwift有三种特性:Single, Maybe和Completable。在对它们一无所知的情况下,你能猜出它们是如何专业化的吗?

Single 将触发成功(值)或错误(错误)事件。成功(值)实际上是下一个事件和已完成事件的组合。这对于成功产生值或失败的一次性进程很有用,比如下载数据或从磁盘加载数据时。

Completable只会发出一个已完成或错误(错误)事件。它不会产生任何值。当您只关心某个操作成功完成或失败时,可以使用completable,例如文件写入。

最后,Maybe是Single和Completable的混搭。它可以发出成功(值)、完成或错误(错误)。如果您需要实现一个可能成功或失败的操作,并在成功时可选地返回一个值,那么Maybe就是您的票据。

你将有机会在第4章“Observables & Subjects in Practice”中更多地研究特性。现在,您将运行一个基本的示例,使用单个文件从资源文件夹中的名为Copyright.txt的文本文件中加载一些文本——因为谁不喜欢偶尔使用一些法律术语呢?

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

example(of: "Single") {
  // 1
  let disposeBag = DisposeBag()

  // 2
  enum FileReadError: Error {
    case fileNotFound, unreadable, encodingFailed
  }
  
  // 3
  func loadText(from name: String) -> Single<String> {
    // 4
    return Single.create { single in
    
    }
  }
}

在上述代码中,您:

  1. 创建一个稍后使用的dispose bag。

  2. 定义一个Error枚举来建模从磁盘上的文件读取数据时可能发生的一些错误。

  3. 实现一个返回Single的函数从的磁盘文件中加载文本。

  4. 创建并返回一个Single。

在create闭包中添加以下代码来完成实现:

// 1
let disposable = Disposables.create()

// 2
guard let path = Bundle.main.path(forResource: name, ofType: "txt") else {
  single(.error(FileReadError.fileNotFound))
  return disposable
}

// 3
guard let data = FileManager.default.contents(atPath: path) else {
  single(.error(FileReadError.unreadable))
  return disposable
}

// 4
guard let contents = String(data: data, encoding: .utf8) else {
  single(.error(FileReadError.encodingFailed))
  return disposable
}

// 5
single(.success(contents))
return disposable

有了这个代码,你:

  1. 创建一个Disposable,因为Create的订阅闭包期望它作为返回类型。

  2. 获取文件名的路径,或者将文件未找到错误添加到Single中,并返回您创建的一次性文件。

  3. 从该路径下的文件获取数据,或者将不可读错误添加到Single中并返回可丢弃的。

  4. 将数据转换为字符串;否则,将编码失败错误添加到Single中,并返回可丢弃的。发现规律了吗?

  5. 走了这么远?成功地将内容添加到Single中,并返回一次性的。

现在可以用这个函数了。将以下代码添加到示例中:

// 1
loadText(from: "Copyright")
// 2
  .subscribe {
    // 3
    switch $0 {
    case .success(let string):
      print(string)
    case .error(let error):
      print(error)
    }
  }
  .disposed(by: disposeBag)
  1. 调用loadText(from:)并传递文本文件的根名称。

  2. 订阅它返回的Single。

  3. 打开事件,如果成功则打印字符串,如果失败则打印错误。

你应该会看到从文件打印到控制台的文本,这与操场底部的版权注释相同:

--- Example of: Single ---
Copyright (c) 2020 Razeware LLC
...

尝试将文件名更改为其他文件名,您应该会看到打印的文件未找到错误。