第8章:观察者模式

观察者模式让一个对象观察另一个对象的变化。苹果在Swift 5.1中增加了对这种模式的语言级支持,在Combine框架中增加了Publisher。

这种模式涉及三种类型。

  1. 订阅者是 "观察者 "对象,接收更新。

  2. 发布者是 "可观察 "对象并发送更新。

  3. 值是被改变的基础对象。

什么时候应该使用它?

只要你想接收另一个对象上的变化,就可以使用观察者模式。

这种模式经常被用于MVC,其中视图控制器有订阅者,模型有发布者。这允许模型将变化传达给视图控制器,而不需要知道任何关于视图控制器的类型。因此,不同的视图控制器可以使用和观察同一模型类型的变化。

Playground例子

打开Starter目录下的FundamentalDesignPattern.xcworkspace,或者从上一章中你自己的playground工作区继续,然后打开Overview页面。

你会看到Observer被列在Behavioral Patterns下面。这是因为观察者是关于一个物体观察另一个物体的。

点击Observer链接,打开该页面。

然后,输入下面的代码示例。

以下是你的做法。

  1. 首先,你导入Combine,它包括@Published注释和Publisher & Subscriber类型。

  2. 接下来,你声明一个新的用户类;@Published属性不能用于结构体或除类之外的任何其他类型。

  3. 接下来,你为name创建一个var属性,并将其标记为@Published。这告诉Xcode为这个属性自动生成一个Publisher。注意,你不能对let属性使用@Published,因为根据定义,它们不能被改变。

  4. 最后,你要创建一个初始化器来设置self.name的初始值。接下来,在playground的末尾添加以下代码。

以下是你的做法。

  1. 首先,你创建了一个名为Ray的新用户。

  2. 接下来,你通过user.$name访问发布者,以广播该用户的名字的变化。这将返回一个类型为Posted.Publisher的对象。这个对象是可以被监听到的更新。

  3. 接下来,你通过调用发布者的sink来创建一个订阅者。这需要一个闭包,在初始值和值发生变化时调用。默认情况下,sink返回一个AnyCancellable类型。然而,你明确地将这个类型声明为AnyCancellable? 以使其成为可选的,因为你以后会将其清空。

  4. 最后,你把用户的名字改为Vicki。

作为回应,你应该看到以下内容被打印到控制台。

User's name is Ray 
User's name is Vicki

在后面添加如下代码:

subscriber = nil 
user.name = "Ray has left the building"

通过将订阅者设置为nil,它将不再接收来自发布者的更新。为了证明这一点,你最后一次改变了用户的名字,但你不会在控制台中看到任何新的输出。

你应该注意些什么?

在你实现观察者模式之前,先确定你期望改变的内容和条件。如果你不能确定一个对象或属性改变的原因,你最好不要把它声明为var或@Published,而应该把它变成一个let属性。

例如,一个唯一的标识符,作为一个公布的属性是没有用的,因为从定义上来说,它不应该改变。

教程项目

你将继续上一章的Rabble Wabble应用程序。

如果你跳过了上一章,或者你想重新开始,请打开Finder并导航到你下载本章资源的地方。然后,在Xcode中打开starter ▸ RabbleWabble ▸ RabbleWabble.xcodeproj。

你将使用观察者模式在 "选择题组 "屏幕上显示用户的最新分数。

从文件层次结构中打开QuestionGroup.swift。这已经有了一个分数,但目前还不能观察它的变化。在导入Foundation下面添加以下内容。

import Combine

这就导入了苹果的新Combine框架,为你完成了所有繁重的工作。

接下来,在Score类(位于QuestionGroup类内)的末尾,在其其他属性之后添加以下内容,暂时忽略编译器错误。

@Published public var runningPercentage: Double = 0

runningPercentage属性将允许观察该问题组的最新 "运行百分比得分"。

编译器目前正在抛出一个错误,因为该属性被标记为@Published,而且它不知道如何自动编码或解码它。要解决这个问题,请在Score的init之后添加以下代码。

下面是这个的作用。

  1. 你首先声明一个CodingKeys的枚举以及correctCount和incorrectCount的情况。这告诉编译器在它自动生成的编码器和解码器方法中忽略runningPercentage。

  2. 在一个Score被解码的情况下,你需要实际设置runningPercentage。要做到这一点,你要为init(from decoder:)创建一个自定义的初始化器,并在设置correctCount和incorrectCount后调用updateRunningPercentage()。

  3. 在updateRunningPercentage()中,你根据correctCount与totalCount的比率来设置runningPercentage。

接下来,将var correctCount和var incorrectCount两行替换为以下内容。

虽然你可以将correctCount和incorrectCount标记为@Published,但你对单独观察这些属性不感兴趣。相反,你感兴趣的是它们如何影响runningPercentage。所以在didSet中,你为每个属性调用updateRunningPercentage()。

在你开始为runningPercentage创建订阅者之前,你需要做一些小改动。首先,在Score类的结尾大括号前添加以下方法。

虽然你可以将correctCount和incorrectCount标记为@Published,但你对单独观察这些属性不感兴趣。相反,你感兴趣的是它们如何影响runningPercentage。所以在didSet中,你为每个属性调用updateRunningPercentage()。

在你开始为runningPercentage创建订阅者之前,你需要做一些小改动。首先,在Score类的结尾大括号前添加以下方法。

public func reset() { 
  correctCount = 0 
  incorrectCount = 0 
}

这个方法 "重置 "了Score。你将在用户重新启动一个问题组时使用它。

接下来,将var score一行替换为以下内容,忽略由此产生的编译器错误。

public private(set) var score: Score

这可以防止所有外部类直接设置分数。这可以确保任何runningPercentage订阅者不会意外地被抹去,如果直接设置分数,就会发生这种情况。

目前有一个地方可以直接设置分数。打开BaseQuestionStrategy.swift,替换下面一行。

self.questionGroupCaretaker.selectedQuestionGroup.score = 
QuestionGroup.Score()

使用如下代码:

self.questionGroupCaretaker.selectedQuestionGroup.score.reset()

构建并运行以确保你没有任何编译器错误。到目前为止,似乎没有什么变化,但你现在已经准备好注册你的观察者了

你首先需要一个地方来保存订阅者对象。理想情况下,这应该与它相关的对象的生命联系在一起。在这种情况下,这就是QuestionGroupCell本身。打开QuestionGroupCell.swift,添加以下内容 import UIKit:

import Combine

接下来,在其他属性之后添加以下属性。

public var percentageSubscriber: AnyCancellable?

然后,打开SelectQuestionGroupViewController.swift,在tableView(_:cellForRowAt:)中加入以下代码,就在返回语句之前。

以下是其作用。

  1. 将cell.percentSubscriber设置为所创建的订阅者。因此,如果单元格被释放,它的订阅者也会自动被释放,它将不会收到更新。

  2. 调用receive(on:)并传递DispatchQueue.main以确保事件被传递到主队列。虽然目前在应用程序中没有使用任何后台线程,但最好总是确保你的UI调用是在主队列上进行的,以防止将来出现问题。

  3. 使用地图将数值转换为百分比字符串。

  4. 调用assign将值设置为cell.percentLabel上的文本。每当数值发生变化,这也会自动更新标签的文本。

建立并运行,选择任何你想要的问题组单元格,并点击几次 "正确 "和 "不正确 "按钮。当你按下 "菜单 "按钮时,分数就会显现出来。更好的是,如果你退出应用程序并重新启动,由于你实现了上一章的纪念品模式,分数将被持续保留。

关键点

你在本章中了解了观察者模式。下面是它的关键点。

RabbleWabble的功能正变得越来越丰富。然而,有一个功能将是非常棒的:用户能够创建他们自己的问题组。你将使用另一种模式来实现这个功能:构建器设计模式。

继续看下一章,了解建造者模式并完成RabbleWabble应用程序。


上一章 目录 下一章