这是一篇已经在我的博客发布的文章-Swift by Sundell。相比在这里阅读,我更推荐在我的博客,因为你可以看到高亮的语法、主题或者更多。
当我们构建App时,经常会发现我们需要处理对象之间一对多的情况。它可能是在同一种情况下多个对象都需要发生变化,或者当某个事件需要被广播到系统的不同部分。
在这些情况下,为特定的对象添加监听是非常普遍的。像大多数编程技术一样,在Swift中也有很多种为对象添加监听的能力—他们都有各自的利弊。这一周,我们先研究两种技术,下一周我们再研究其他的。
下面我们开始潜水:
实例:
为了更直观的对不同技术进行比较,我们将尽力使用相同的实例。我们将会用AudioPlayer 类来做例子,让其他的对象监听它的状态(PlaybackState)。每当开始播放、暂停或者完成回放,我们希望通知它的监听者完成状态的改变。这样多个对象将逻辑与同一个播放器绑定,比如显示可播放项目的列表、播放器的UI以及在屏幕底部显示“迷你播放器”之类的功能按钮。
下面是AudioPlayer类的代码:
class AudioPlayer {
private var state = State.idle {
// We add a property observer on 'state', which lets us
// run a function on each value change.
didSet { stateDidChange() }
}
func play(_ item: Item) {
state = .playing(item)
startPlayback(with: item)
}
func pause() {
switch state {
case .idle, .paused:
// Calling pause when we're not in a playing state
// could be considered a programming error, but since
// it doesn't do any harm, we simply break here.
break
case .playing(let item):
state = .paused(item)
pausePlayback()
}
}
func stop() {
state = .idle
stopPlayback()
}
}
从上面可以看出我们使用在之前在Modelling state in Swift中介绍的技巧,使用枚举来表示AudioPlayer类内部的不同状态。这样可以给我们更加清晰、定义明确的播放器状态,而不是可选或者多种真相。
下面是我定义AudioPlayer状态的枚举:
private extension AudioPlayer {
enum State {
case idle
case playing(Item)
case paused(Item)
}
}
在上面你可以看到每当播放状态变化的时候,每次我会调用方法,stateDidChange(),我们的主要工作就是使用不同的技术,不一样的实现方法来填充这个方法。
通知中心:
首先要看的就是系统NotificationCenter,每当播放状态改变的时候,我们就用它的API来发送广播。想大多数系统级别的API,NotificationCenter也是一个单例,为了明确表示我们依赖NotificationCenter来对AudioPlayer类来做测试,我们将在AudioPlayer类中注入一个notification center的实例,如下:
class AudioPlayer {
private let notificationCenter: NotificationCenter
init(notificationCenter: NotificationCenter = .default) {
self.notificationCenter = notificationCenter
}
}
如果想学习更多的依赖注入,可以查看:“Different flavors of dependency injection in Swift",
NotificationCenter将使用不同的通知名字来区分事件的监听或者发生。为了避免使用内联字符串作为API的一部分,我们将为NotificationCenter.Name添加扩展,来获得单一的消息来源的通知名字,我将添加一个回放开始,一个暂停、一个停止:
extension Notification.Name {
static var playbackStarted: Notification.Name {
return .init(rawValue: "AudioPlayer.playbackStarted")
}
static var playbackPaused: Notification.Name {
return .init(rawValue: "AudioPlayer.playbackPaused")
}
static var playbackStopped: Notification.Name {
return .init(rawValue: "AudioPlayer.playbackStopped")
}
}
上面的扩展将帮助更方便的使用点语法来调用API来获取通知的名字,像.playbackStarted,使用起来非常不错。
通过上面添加扩展,现在可以发送通知了。我们检查当前的状态看看我们需要发送什么通知,将之前的stateDidChange()方法补充完整。不仅是播放和暂停的状态,我们也需要把当前播放的item作为通知对象:
private extension AudioPlayer {
func stateDidChange() {
switch state {
case .idle:
notificationCenter.post(name: .playbackStopped, object: nil)
case .playing(let item):
notificationCenter.post(name: .playbackStarted, object: item)
case .paused(let item):
notificationCenter.post(name: .playbackPaused, object: item)
}
}
}
就是这样!在此代码基础上,我们可以在任何地方轻松监听到当前录像播放的状态。举个例子,下面是可能会有一个控制器NowPlayingViewController监听何时录像播放开始,进而显示当前的名字和总时间。
class NowPlayingViewController: UIViewController {
deinit {
// If your app supports iOS 8 or earlier, you need to manually
// remove the observer from the center. In later versions
// this is done automatically.
notificationCenter.removeObserver(self)
}
override func viewDidLoad() {
super.viewDidLoad()
notificationCenter.addObserver(self,
selector: #selector(playbackDidStart),
name: .playbackStarted,
object: nil
)
}
@objc private func playbackDidStart(_ notification: Notification) {
guard let item = notification.object as? AudioPlayer.Item else {
let object = notification.object as Any
assertionFailure("Invalid object: \(object)")
return
}
titleLabel.text = item.title
durationLabel.text = "\(item.duration)"
}
}
使用通知中心的优势就是非常容易实现,不仅是对象内部被监听,而且是任何一个想要开始监听它的内部。通知也是大多数Swift开发者经常使用的API,而且苹果自身也在使用它来发送系统的通知,比如键盘的事件。
然而,这种方法也有明显的缺点。第一:尽管NotificationCenter是Objective-C API,不能使用Swift的特性,像泛型一样保持类型安全。尽管可以像上面一样实现(通过创建某种形式的解包),默认的方式是需要我们像上面前几行playbackDidStart那样进行类型转换。这样是我们的代码非常脆弱,因为我们无法利用编译器来确保我们的观察者和被观察的对象使用相同类型的广播值。
说到广播,使用广播的另一个缺点就是通知没有任何限制,在应用内都可以监听到。虽然这样很方便我们在任何地方对观察的对象进行监听,但是它也使参与观察的对象之间的关系变得更好松散,使得应用程序的各个部分之间更加无法保持清晰的分离,尤其是在代码增长的时候。
Observation protocols
下面,让我们看看如何通过协议来构建一个更加严格标准、定义明确的监听API。当我们用这个技术的时候需要想要监AudioPlayer的对象遵守AudioPlayerObserver协议。就像我定义了三种通知为录像播放的每一种状态,我将定义了三种方法来监听每一种事件,就像这样:
protocol AudioPlayerObserver: class {
func audioPlayer(_ player: AudioPlayer,
didStartPlaying item: AudioPlayer.Item)
func audioPlayer(_ player: AudioPlayer,
didPausePlaybackOf item: AudioPlayer.Item)
func audioPlayerDidStop(_ player: AudioPlayer)
}
为了只监听一个事件,我将使用协议扩展为每个事件来添加默认实现。
extension AudioPlayerObserver {
func audioPlayer(_ player: AudioPlayer,
didStartPlaying item: AudioPlayer.Item) {}
func audioPlayer(_ player: AudioPlayer,
didPausePlaybackOf item: AudioPlayer.Item) {}
func audioPlayerDidStop(_ player: AudioPlayer) {}
}
弱存储
当设计监听API的时候,通常好的做法就是对所有的监听者保持弱饮用。否则,当观察对象同时在观察自己时很容易造成循环饮用。然而,在Swift集合中用不错的方式弱引用存储对象不是最直接的方式,因为,集合默认对所有的成员保持强引用。
为了解决监听需求带来的问题,我将介绍一个小解包类型,对监听者只保持简单的弱引用状态。
private extension AudioPlayer {
struct Observation {
weak var observer: AudioPlayerObserver?
}
}
使用上面的类型,现在我们给AudioPlayer添加一个监听集合。在这种情况下,我们将用一个键是ObjectIdentifier的字典插入和删除观察者们:
class AudioPlayer {
private var observations = ObjectIdentifier : Observation
}
ObjectIdentifier是系统内置的值类型,可以作为类对象实例的唯一表示。想要了解更多-请学习“Identifying objects in Swift”。
我们可以在stateDidChange()中遍历所有的监听,并且调用相对应状态的协议来实现。而且值得注意的是,当我们在迭代的同时,还可以清除那些没有使用的观察值(如果相应的对象已经被释放销毁)。
private extension AudioPlayer {
func stateDidChange() {
for (id, observation) in observations {
// If the observer is no longer in memory, we
// can clean up the observation for its ID
guard let observer = observation.observer else {
observations.removeValue(forKey: id)
continue
}
switch state {
case .idle:
observer.audioPlayerDidStop(self)
case .playing(let item):
observer.audioPlayer(self, didStartPlaying: item)
case .paused(let item):
observer.audioPlayer(self, didPausePlaybackOf: item)
}
}
}
}
观察
最后,我们需要对象实现AudioPlayerObserver协议来注册自己成为观察者.我们同样需要一个简单取消是监听者的方式,以防监听者不再需要监听状态更新。为了实现这来两种情况,我们将会
AudioPlayer添加一个包含两个方法的扩展:
extension AudioPlayer {
func addObserver(_ observer: AudioPlayerObserver) {
let id = ObjectIdentifier(observer)
observations[id] = Observation(observer: observer)
}
func removeObserver(_ observer: AudioPlayerObserver) {
let id = ObjectIdentifier(observer)
observations.removeValue(forKey: id)
}
}
非常棒!我们可以用新的监听协议代替通知来更新NowPlayingViewController:
class NowPlayingViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
player.addObserver(self)
}
}
extension NowPlayingViewController: AudioPlayerObserver {
func audioPlayer(_ player: AudioPlayer,
didStartPlaying item: AudioPlayer.Item) {
titleLabel.text = item.title
durationLabel.text = "\(item.duration)"
}
}
正如上面你看到的那样,相比通知,使用监听协议的主要优势是我们确保编译时的类型安全。由于使用协议时直接调用AudiPlayer.Item类型,不需要在观察的方法中进行类型转换,是我们的代码更加清晰。
添加一个明显的监听API也可以帮助理解监听类是如何工作的,而不需要弄清楚NotificationCenter应该使用什么。通过我们的API例子,你应该支持使用AudioPlayerObserver协议去监听。
然而,这种方法也有它自身的缺点,相比通知那就是在内部使用了更多的代码。它需要引入额外的协议和类型,如果代码非常依赖于观察结果,这可能就是一个缺点。
未完待续:第二部分!