多播委托模式是一种行为模式,是委托模式的一种变体。它允许你创建一对多的委托关系,而不是简单委托中的一对一关系。它涉及四种类型。
一个需要委托的对象,也被称为委托对象,是拥有一个或多个委托的对象。
委托协议规定了一个委托人可以或应该实现的方法。
委托人是实现委托协议的对象。
多播委托是一个辅助类,它可以持有委托,并允许你在值得委托的事件发生时通知每个委托。
多播委托模式和委托模式的主要区别在于有一个多播委托的辅助类。Swift默认不会为你提供这个类。然而,你可以很容易地创建你自己的,你将在本章中这样做。
注意:苹果在Swift 5.1的Combine框架中引入了一个新的多播类型。这与本章中介绍的MulticastDelegate不同。它允许你处理多个发布者事件。因此,这可以作为多播委托模式的替代品,作为反应式架构的一部分。
多播是Combine框架中的一个高级话题,它超出了本章的范围。如果你想了解更多关于Combine的信息,请查看我们关于它的书,Combine。Asynchronous Programming with Swift(http://bit.ly/swift-combine)。
使用这种模式来创建一对多的委托关系。
例如,你可以使用这种模式来通知多个对象,只要另一个对象发生了变化。然后,每个委托可以更新自己的状态或执行相关动作作为回应。
打开Starter目录下的IntermediateDesignPattern.xcworkspace,或者从上一章中你自己的playground工作区继续,然后从文件层次结构中打开MulticastDelegate页面。
在你编写本页面的代码示例之前,你需要创建MulticastDelegate辅助类。
在Source下,打开MulticastDelegate.swift并添加以下代码。
以下是这段代码中的内容。
你把MulticastDelegate定义为一个通用类,接受任何ProtocolType作为通用类型。Swift还没有提供一种方法来限制
你把DelegateWrapper定义为一个内类。你将用它来包装委托对象作为一个弱属性。这样一来,组播委托就可以抓住强包装器实例,而不是直接抓住委托。
不幸的是,在这里你必须将委托属性声明为AnyObject而不是ProtocolType。这是因为弱变量必须是AnyObject(也就是一个类)。你会认为你可以在泛型定义中把 ProtocolType 声明为 AnyObject。但是这行不通,因为你需要传递一个协议作为类型,而它本身并不是一个对象。
接下来,在MulticastDelegate的结束类大括号前添加以下内容。
依次看每个评论部分。
你声明delegateWrappers以保持DelegateWrapper实例,这些实例将由MulticastDelegate从传递给它的委托中创建。
然后你为委托人添加一个计算属性。这将从delegateWrappers中过滤出已经被释放的委托,然后返回一个不确定的非空的委托数组。
最后,你要创建一个初始化器,接受一个代表数组,并将这些代表映射到创建delegateWrappers。
在MulticastDelegate被创建后,你还需要一种方法来添加和删除代表。在前面的代码后添加以下实例方法来实现这个目的。
以下是该代码的作用。
顾名思义,你将使用addDelegate来添加一个委托实例,它将创建一个DelegateWrapper并将其附加到delegateWrappers中。
同样地,你将使用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
从文件层次结构中打开AcceptInputState.swift。这个类是由DrawView使用的,它负责创建线和点以响应用户的触摸。你也要更新它来通知绘图视图的委托人。
将touchesBegan(_:event:)改为以下内容。
你没有在touchesBegan(:event:)中直接追加新的线条并将其添加到drawView.layer中,而是将这个逻辑移到一个新的辅助方法中,addLine(:)。这将允许你以后单独调用addLine(:)与touchesBegan(:event:)。
你调用drawView.multicastDelegate.invokeDelegates来通知所有的人,一个新的线条已经被创建。新的线条已经被创建。
接下来,将touchesMoved(_:event:)替换为以下内容。
你在这里也做了两个类似的改动。
你现在没有直接在touchesMoved(:event:)中添加点,而是调用addPoint( point:)。同样,这也是为了让你以后能够单独调用它。
每当一个新的点被创建时,你就通知所有的委托人。
很好,这就解决了委托人的通知问题。接下来,你需要在某处实际符合新的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。建立并运行,并尝试在左上角的绘图视图中绘图。其他的视图现在应该在你绘图的时候被实时更新
你在本章中了解了多播委托模式。下面是它的关键点。
多播委托模式允许你创建一对多的委托关系。它涉及四种类型:需要委托的对象、委托协议、委托人和多播委托。
一个需要委托的对象有一个或多个委托;委托协议规定了一个委托应该实现的方法;委托实现了委托协议;而多播委托是一个帮助类,用于保持和通知委托人。
Swift并没有为你提供一个多播委托对象。然而,你可以很容易地实现你自己的委托来支持这种模式。
镜面垫现在真的很实用了! 然而,现在还没有办法与世界分享你惊人的创作...。
上一章 | 目录 | 下一章 |
---|