第16章:多播委托模式

多播委托模式是一种行为模式,是委托模式的一种变体。它允许你创建一对多的委托关系,而不是简单委托中的一对一关系。它涉及四种类型。

  1. 一个需要委托的对象,也被称为委托对象,是拥有一个或多个委托的对象。

  2. 委托协议规定了一个委托人可以或应该实现的方法。

  3. 委托人是实现委托协议的对象。

  4. 多播委托是一个辅助类,它可以持有委托,并允许你在值得委托的事件发生时通知每个委托。

多播委托模式和委托模式的主要区别在于有一个多播委托的辅助类。Swift默认不会为你提供这个类。然而,你可以很容易地创建你自己的,你将在本章中这样做。

注意:苹果在Swift 5.1的Combine框架中引入了一个新的多播类型。这与本章中介绍的MulticastDelegate不同。它允许你处理多个发布者事件。因此,这可以作为多播委托模式的替代品,作为反应式架构的一部分。

多播是Combine框架中的一个高级话题,它超出了本章的范围。如果你想了解更多关于Combine的信息,请查看我们关于它的书,Combine。Asynchronous Programming with Swift(http://bit.ly/swift-combine)。

你什么时候应该使用它?

使用这种模式来创建一对多的委托关系。

例如,你可以使用这种模式来通知多个对象,只要另一个对象发生了变化。然后,每个委托可以更新自己的状态或执行相关动作作为回应。

playground示例

打开Starter目录下的IntermediateDesignPattern.xcworkspace,或者从上一章中你自己的playground工作区继续,然后从文件层次结构中打开MulticastDelegate页面。

在你编写本页面的代码示例之前,你需要创建MulticastDelegate辅助类。

在Source下,打开MulticastDelegate.swift并添加以下代码。

以下是这段代码中的内容。

  1. 你把MulticastDelegate定义为一个通用类,接受任何ProtocolType作为通用类型。Swift还没有提供一种方法来限制只适用于协议。因此,你可以为 ProtocolType 传递一个具体的类的类型而不是协议。然而,最可能的是,你会使用一个协议。因此,你将通用类型命名为 ProtocolType,而不仅仅是 Type。

  2. 你把DelegateWrapper定义为一个内类。你将用它来包装委托对象作为一个弱属性。这样一来,组播委托就可以抓住强包装器实例,而不是直接抓住委托。

不幸的是,在这里你必须将委托属性声明为AnyObject而不是ProtocolType。这是因为弱变量必须是AnyObject(也就是一个类)。你会认为你可以在泛型定义中把 ProtocolType 声明为 AnyObject。但是这行不通,因为你需要传递一个协议作为类型,而它本身并不是一个对象。

接下来,在MulticastDelegate的结束类大括号前添加以下内容。

依次看每个评论部分。

  1. 你声明delegateWrappers以保持DelegateWrapper实例,这些实例将由MulticastDelegate从传递给它的委托中创建。

  2. 然后你为委托人添加一个计算属性。这将从delegateWrappers中过滤出已经被释放的委托,然后返回一个不确定的非空的委托数组。

  3. 最后,你要创建一个初始化器,接受一个代表数组,并将这些代表映射到创建delegateWrappers。

在MulticastDelegate被创建后,你还需要一种方法来添加和删除代表。在前面的代码后添加以下实例方法来实现这个目的。

以下是该代码的作用。

  1. 顾名思义,你将使用addDelegate来添加一个委托实例,它将创建一个DelegateWrapper并将其附加到delegateWrappers中。

  2. 同样地,你将使用removeDelegate来移除一个委托。在这种情况下,你首先要尝试找到与委托对象相匹配的DelegateWrapper的索引,使用指针等价法,即===而不是==,如果找到了,你就删除给定索引的委托对象。

最后,你需要一个方法来实际调用所有的委托。在前面的方法之后添加以下方法。

public func invokeDelegates(_ closure: (ProtocolType) -> ()) { 
  delegates.forEach { closure($0) } 
}

你通过delegates进行迭代,你之前定义的计算属性会自动屏蔽掉nil实例,并在每个delegate实例上调用传入的closure。

太棒了--你现在有了一个非常有用的MulticastDelegate辅助类,并准备好试用它了。

从文件层次结构中打开MulticastDelegate页面,并在代码示例后输入以下内容。

// MARK: - Delegate Protocol 
public protocol EmergencyResponding {
  func notifyFire(at location: String)
  func notifyCarCrash(at location: String) 
}

你定义了EmergencyResponding,它将作为委托协议。

接下来,添加以下内容。

你定义了两个委托对象。FireStation和PoliceStation。每当有紧急情况发生时,警察和消防员都会做出反应。

为了简单起见,每当这些对象上的方法被调用时,你只需打印出信息。接下来,在playground的末尾添加以下代码。

// MARK: - Delegating Object 
public class DispatchSystem {
  let multicastDelegate =
    MulticastDelegate<EmergencyResponding>() 
}

你声明DispatchSystem,它有一个multicastDelegate属性。这就是委托对象。你可以想象这是一个更大的调度系统的一部分,每当发生火灾、车祸或其他紧急事件时,你都会通知所有的紧急救援人员。

接下来,在playground的末尾添加以下代码。

// MARK: - Example 
let dispatch = DispatchSystem() 
var policeStation: PoliceStation! = PoliceStation() 
var fireStation: FireStation! = FireStation()

dispatch.multicastDelegate.addDelegate(policeStation) 
dispatch.multicastDelegate.addDelegate(fireStation)

你将 dispatch 创建为 DispatchSystem 的一个实例。然后,你为policeStation和fireStation创建委托实例,并通过调用dispatch.multicastDelegate.addDelegate(_:)来注册这两个实例。

接下来,在playground的末尾添加以下代码。

dispatch.multicastDelegate.invokeDelegates { 
  $0.notifyFire(at: "Ray’s house!") 
}

这就在multicastDelegate的每个委托实例上调用notifyFire(at:)。你应该看到以下内容被打印到控制台。

Police were notified about a fire at Ray's house!
Firefighters were notified about a fire at Ray's house!

哦,不,雷的房子里发生了火灾!我希望他没事。我希望他没事。

如果一个委托人变成了零,它就不应该被通知到任何未来对多播委托的调用。最后,接下来添加以下内容,以验证这是否能按预期工作。

print("") fireStation = nil

dispatch.multicastDelegate.invokeDelegates { 
   $0.notifyCarCrash(at: "Ray's garage!") 
}

你把fireStation设置为nil,这又会导致它在MulticastDelegate上的相关DelegateWrapper的委托也被设置为nil。当你调用invokeDelegates时,这将导致上述DelegateWrapper被过滤掉,所以它的委托代码不会被调用。

你应该看到这个信息被打印到控制台。

警察接到通知说雷的车库里发生了一起车祸!

雷一定是在试图逃出火场时滑出了车道! 快离开那里,雷!

你应该注意什么?

这种模式对 "只提供信息 "的委托人调用效果最好。

如果委托人需要提供数据,这种模式就不好用了。这是因为多个委托人会被要求提供数据,这可能会导致重复的信息或浪费的处理。

在这种情况下,可以考虑使用责任链模式来代替,这将在后面的章节中介绍。

教程项目

你将继续上一章中的Mirror Pad应用。

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

建立并运行该应用程序。在左上角的视图中画几条线,然后按Animate键,可以看到画到每个视图的线。这很不错,但是如果在你画线的过程中加入线条,那不是很酷吗?你肯定会的!这正是你要做的。这正是你在本章中要添加的内容。

要做到这一点,你将需要在Playground例子中创建的MulticastDelegate.Swift文件。如果你跳过了Playground的例子,请打开Finder,导航到你下载本章资源的地方,并打开final/ IntermediateDesignPatterns.xcworkspace。否则,请随意使用你自己的playground上的file。

回到MirrorPad.xcodeproj,在协议组中创建一个名为MulticastDelegate.swift的新文件。然后,从IntermediateDesignPatterns.xcworkspace中复制并粘贴MulticastDelegate.swift的全部内容,并将其粘贴到新创建的文件。

接下来,从文件层次结构中打开DrawView.swift,在文件顶部的导入部分之后添加以下内容。

@objc public protocol DrawViewDelegate: class { 
  func drawView(_ source: DrawView, didAddLine line: LineShape) 
  func drawView(_ source: DrawView, didAddPoint point: CGPoint) 
}

DrawViewDelegate将是委托协议。每当有新的线或点被添加时,你将通知所有的委托实例。

接下来,在DrawView的关闭类大括号前添加以下内容。

// MARK: - Delegate Management 
public let multicastDelegate = 
  MulticastDelegate<DrawViewDelegate>()
  
public func addDelegate(_ delegate: DrawViewDelegate) { 
    multicastDelegate.addDelegate(delegate) 
}

public func removeDelegate(_ delegate: DrawViewDelegate) { 
  multicastDelegate.removeDelegate(delegate) 
}

你创建了一个MulticastDelegate的新实例,叫做multicastDelegate,还有两个方便的方法来添加和删除委托,addDelegate(:)和removeDelegate(:)。

从文件层次结构中打开AcceptInputState.swift。这个类是由DrawView使用的,它负责创建线和点以响应用户的触摸。你也要更新它来通知绘图视图的委托人。

将touchesBegan(_:event:)改为以下内容。

与以前的实施相比,你做了两个重大的改变。
  1. 你没有在touchesBegan(:event:)中直接追加新的线条并将其添加到drawView.layer中,而是将这个逻辑移到一个新的辅助方法中,addLine(:)。这将允许你以后单独调用addLine(:)与touchesBegan(:event:)。

  2. 你调用drawView.multicastDelegate.invokeDelegates来通知所有的人,一个新的线条已经被创建。新的线条已经被创建。
    接下来,将touchesMoved(_:event:)替换为以下内容。

你在这里也做了两个类似的改动。

  1. 你现在没有直接在touchesMoved(:event:)中添加点,而是调用addPoint( point:)。同样,这也是为了让你以后能够单独调用它。

  2. 每当一个新的点被创建时,你就通知所有的委托人。

很好,这就解决了委托人的通知问题。接下来,你需要在某处实际符合新的DrawViewDelegate协议。

在这样做之前,你必须了解MirrorPad是如何实际使用DrawView的。它有多个DrawView实例,显示输入DrawView的 "镜像"。每个镜像DrawView实例之间的区别是它们的layer.sublayerTransform,它决定了它们的镜像变换。

为了在主DrawView对象更新时更新镜像DrawView对象,你需要让DrawView本身符合DrawViewDelegate。然而,DrawView应该只在其currentState被设置为AcceptInputState时接受新的线条和点。这可以防止在动画运行时添加线条或点等事情导致的潜在问题。

因此,你也需要让DrawView的基本状态DrawViewState符合DrawViewDelegate。这可以让AcceptInputState覆盖委托方法并正确处理新的线和点。

你让DrawViewState符合DrawViewDelegate,并为这两个必要的方法提供空的实现。结果是,如果DrawViewState目前不是AcceptInputState,那么这些调用就不会有任何作用。

接下来,打开AcceptInputState.swift,在文件的末尾添加以下内容。

在drawView(_:didAddLine:)中,你通过复制传入的线来创建一个新的线,然后调用addLine来添加它。你需要复制直线,以便让它同时显示在原始DrawView和这个DrawView本身。

在drawView(:didAddPoint:)中,你只需调用addPoint(:)来添加点。因为CGPoint是一个结构体,它使用了值语义,所以它被自动复制了。

接下来你需要让DrawView本身符合DrawViewDelegate。打开DrawView.swift,把这个添加到文件的最后。

你只需将调用传递给currentState。

你几乎已经准备好了,可以试一试了。你需要做的最后一件事是把 "镜像 "DrawViews注册为输入DrawView的代理。

打开ViewController.swift,在现有的属性后面添加以下内容。

你只需遍历每个mirrorDrawView,并将它们作为委托添加到inputDrawView。建立并运行,并尝试在左上角的绘图视图中绘图。其他的视图现在应该在你绘图的时候被实时更新

关键点

你在本章中了解了多播委托模式。下面是它的关键点。

镜面垫现在真的很实用了! 然而,现在还没有办法与世界分享你惊人的创作...。


上一章 目录 下一章