Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

16. Multicast Delegate Pattern
Written by Joshua Greene

The multicast delegate pattern is a behavioral pattern that’s a variation on the delegate pattern. It allows you to create one-to-many delegate relationships, instead of one-to-one relationships in a simple delegate. It involves four types:

  1. An object needing a delegate, also known as the delegating object, is the object that has one or more delegates.

  2. The delegate protocol defines the methods a delegate may or should implement.

  3. The delegate(s) are objects that implement the delegate protocol.

  4. The multicast delegate is a helper class that holds onto delegates and allows you to notify each whenever a delegate-worthy event happens.

The main difference between the multicast delegate pattern and the delegate pattern is the presence of a multicast delegate helper class. Swift doesn’t provide you this class by default. However, you can easily create your own, which you’ll do in this chapter.

Note: Apple introduced a new Multicast type in the Combine framework in Swift 5.1. This is different than the MulticastDelegate introduced in this chapter. It allows you to handle multiple Publisher events. In such, this could be used as an alternative to the multicast delegate pattern as part of a reactive achitecture.

Multicast is an advanced topic in the Combine framework, and it’s beyond the scope of this chapter. If you’d like to learn more about Combine, check out our book about it, Combine: Asynchronous Programming with Swift (http://bit.ly/swift-combine).

When should you use it?

Use this pattern to create one-to-many delegate relationships.

For example, you can use this pattern to inform multiple objects whenever a change has happened to another object. Each delegate can then update its own state or perform relevant actions in response.

Playground example

Open IntermediateDesignPattern.xcworkspace in the Starter directory, or continue from your own playground workspace from the last chapter, and then open the MulticastDelegate page from the File hierarchy.

Before you can write the Code example for this page, you need to create the MulticastDelegate helper class.

Under Sources, open MulticastDelegate.swift and add the following code:

// 1
public class MulticastDelegate<ProtocolType> {

  // MARK: - DelegateWrapper
  // 2
  private class DelegateWrapper {

    weak var delegate: AnyObject?

    init(_ delegate: AnyObject) {
      self.delegate = delegate
    }
  }
}

Here’s what’s going on in this code:

  1. You define MulticastDelegate as a generic class that accepts any ProtocolType as the generic type. Swift doesn’t yet provide a way to restrict <ProtocolType> to protocols only. Consequently, you could pass a concrete class type instead of a protocol for ProtocolType. Most likely, however, you’ll use a protocol. Hence, you name the generic type as ProtocolType instead of just Type.

  2. You define DelegateWrapper as an inner class. You’ll use this to wrap delegate objects as a weak property. This way, the multicast delegate can hold onto strong wrapper instances, instead of the delegates directly.

Unfortunately, here you have to declare the delegate property as AnyObject instead of ProtocolType. That’s because weak variables have to be AnyObject (i.e., a class). You’d think you could just declare ProtocolType as AnyObject in the generic definition. However that won’t work because you’ll need to pass a protocol as the type, which itself isn’t an object.

Next, add the following right before the closing class curly brace for MulticastDelegate:

// MARK: - Instance Properties
// 1
private var delegateWrappers: [DelegateWrapper]

// 2
public var delegates: [ProtocolType] {
  delegateWrappers = delegateWrappers
    .filter { $0.delegate != nil }
  return delegateWrappers.map
    { $0.delegate! } as! [ProtocolType]
}

// MARK: - Object Lifecycle
// 3
public init(delegates: [ProtocolType] = []) {
  delegateWrappers = delegates.map {
    DelegateWrapper($0 as AnyObject)
  }
}

Taking each commented section in turn:

  1. You declare delegateWrappers to hold onto the DelegateWrapper instances, which will be created under the hood by MulticastDelegate from delegates passed to it.

  2. You then add a computed property for delegates. This filters out delegates from delegateWrappers that have already been released and then returns an array of definitely non-nil delegates.

  3. You lastly create an initializer that accepts an array of delegates and maps these to create delegateWrappers.

You also need a means to add and remove delegates after a MulticastDelegate has been created already. Add the following instance methods after the previous code to do this:

// MARK: - Delegate Management
// 1
public func addDelegate(_ delegate: ProtocolType) {
  let wrapper = DelegateWrapper(delegate as AnyObject)
  delegateWrappers.append(wrapper)
}

// 2
public func removeDelegate(_ delegate: ProtocolType) {
  guard let index = delegateWrappers.firstIndex(where: {
    $0.delegate === (delegate as AnyObject)
  }) else {
    return
  }
  delegateWrappers.remove(at: index)
}

Here’s what that code does:

  1. As its name implies, you’ll use addDelegate to add a delegate instance, which creates a DelegateWrapper and appends it to the delegateWrappers.

  2. Likewise, you’ll use removeDelegate to remove a delegate. In such, you first attempt to find the index for the DelegateWrapper that matches the delegate using pointer equality, === instead of ==. If found, you remove the delegate wrapper at the given index.

Lastly, you need a means to actually invoke all of the delegates. Add the following method after the previous ones:

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

You iterate through delegates, the computed property you defined before that automatically filters out nil instances, and call the passed-in closure on each delegate instance.

Fantastic — you now have a very useful MulticastDelegate helper class and are ready to try it out!

Open the MulticastDelegate page from the File hierarchy, and enter the following after Code example:

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

You define EmergencyResponding, which will act as the delegate protocol.

Next, add the following:

// MARK: - Delegates
public class FireStation: EmergencyResponding {

  public func notifyFire(at location: String) {
    print("Firefighters were notified about a fire at "
      + location)
  }

  public func notifyCarCrash(at location: String) {
    print("Firefighters were notified about a car crash at "
      + location)
  }
}

public class PoliceStation: EmergencyResponding {

  public func notifyFire(at location: String) {
    print("Police were notified about a fire at "
      + location)
  }

  public func notifyCarCrash(at location: String) {
    print("Police were notified about a car crash at "
      + location)
  }
}

You define two delegate objects: FireStation and PoliceStation. Whenever an emergency happens, both the police and fire fighters will respond.

For simplicity, you simply print out messages whenever a method is called on these. Next, add the following code to the end of the playground:

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

You declare DispatchSystem, which has a multicastDelegate property. This is the delegating object. You can imagine this is part of a larger dispatch system, where you notify all emergency responders whenever a fire, crash, or other emergency event happens.

Next, add the following code to the end of the playground:

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

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

You create dispatch as an instance of DispatchSystem. You then create delegate instances for policeStation and fireStation and register both by calling dispatch.multicastDelegate.addDelegate(_:).

Next, add the following code to the end of the playground:

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

This calls notifyFire(at:) on each of the delegate instances on multicastDelegate. You should see the following printed to the console:

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

Oh noes, there’s a fire at Ray’s house! I hope he’s okay.

In the event that a delegate becomes nil, it should not be notified of any future calls on multicast delegate. Finally, add the following next to verify that this works as intended:

print("")
fireStation = nil

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

You set fireStation to nil, which in turn will result in its related DelegateWrapper on MulticastDelegate having its delegate set to nil as well. When you then call invokeDelegates, this will result in said DelegateWrapper being filtered out, so its delegate’s code will not be invoked.

You should see this printed to the console:

Police were notified about a car crash at Ray's garage!

Ray must have skidded off the driveway when he was trying to get out of the fire! Get out of there, Ray!

What should you be careful about?

This pattern works best for “information only” delegate calls.

If delegates need to provide data, this pattern doesn’t work well. That’s because multiple delegates would be asked to provide the data, which could result in duplicated information or wasted processing.

In this case, consider using the chain-of-responsibility pattern instead, which is covered in a later chapter.

Tutorial project

You’ll continue the Mirror Pad app from the previous chapter.

If you skipped the previous chapter, or you want a fresh start, open Finder and navigate to where you downloaded the resources for this chapter. Then, open starter/MirrorPad/MirrorPad.xcodeproj in Xcode.

Build and run the app. Draw several lines into the top-left view and press Animate to see the lines drawn to each view. This is pretty neat, but wouldn’t it be cool if the lines were added as you drew? You bet it would! This is exactly what you’ll be adding in this chapter.

To do so, you’re going to need the MulticastDelegate.Swift file you created in the Playground example. If you skipped the Playground example, open Finder, navigate to where you downloaded the resources for this chapter, and open final/IntermediateDesignPatterns.xcworkspace. Otherwise, feel free to use your own file from the playground.

Back in MirrorPad.xcodeproj, create a new file named MulticastDelegate.swift in the Protocols group. Then, copy and paste the entire contents from MulticastDelegate.swift from the IntermediateDesignPatterns.xcworkspace, and paste this into your newly created file.

Next, open DrawView.swift from the File hierarchy and add the following at the top of the file, after the imports:

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

DrawViewDelegate will be the delegate protocol. You’ll notify all delegate instances whenever a new line or point is added.

Next, add the following right before the closing class curly brace for 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)
}

You create a new instance of MulticastDelegate<DrawViewDelegate> called multicastDelegate and two convenience methods to add and remove delegates, addDelegate(_:) and removeDelegate(_:).

Open AcceptInputState.swift from the File hierarchy. This class is used by DrawView, and it’s responsible for creating lines and points in response to user touches. You’ll update it to notify the draw view’s delegates as well.

Replace touchesBegan(_:event:) with the following:

public override func touchesBegan(_ touches: Set<UITouch>,
                                  with event: UIEvent?) {
  guard let point = touches.first?.location(in: drawView)
    else { return }
  let line = LineShape(color: drawView.lineColor,
                       width: drawView.lineWidth,
                       startPoint: point)
  // 1
  addLine(line)

  // 2
  drawView.multicastDelegate.invokeDelegates {
    $0.drawView(drawView, didAddLine: line)
  }
}

private func addLine(_ line: LineShape) {
  drawView.lines.append(line)
  drawView.layer.addSublayer(line)
}

You made two significant changes from the previous implementation:

  1. Instead of appending the new line and adding it to the drawView.layer directly within touchesBegan(_:event:), you move this logic into a new helper method, addLine(_:). This will allow you to call addLine(_:) separately from touchesBegan(_:event:) later on.

  2. You call drawView.multicastDelegate.invokeDelegates to notify all that a new line has been created.

Next, replace touchesMoved(_:event:) with the following:

public override func touchesMoved(_ touches: Set<UITouch>,
                                  with event: UIEvent?) {
  guard let point = touches.first?.location(in: drawView)
    else { return }

  // 1
  addPoint(point)

  // 2
  drawView.multicastDelegate.invokeDelegates {
    $0.drawView(drawView, didAddPoint: point)
  }
}

private func addPoint(_ point: CGPoint) {
  drawView.lines.last?.addPoint(point)
}

You also made two similar changes here:

  1. Instead of adding the point directly within touchesMoved(_:event:), you now call addPoint(_ point:). Again, this is to enable you to call it separately later on.

  2. You notify all delegates whenever a new point has been created.

Great, this takes care of the delegate notifications! You next need to actually conform to the new DrawViewDelegate protocol somewhere.

Before you can do so, it’s important you understand how MirrorPad actually uses DrawView. It has multiple DrawView instances that displays “mirrors” of the input DrawView. The difference between each mirror DrawView instance is their layer.sublayerTransform, which determines their mirror transformations.

In order to update the mirror DrawView objects whenever the master DrawView object is updated, you’ll need to make DrawView itself conform to DrawViewDelegate. However, DrawView should only accept new lines and points when its currentState is set to AcceptInputState. This prevents potential issues resulting from things such as adding lines or points while the animation is running.

Consequently, you also need to make DrawViewState, the base state used by DrawView, conform to DrawViewDelegate. This lets AcceptInputState override the delegate methods and handle the new lines and points correctly.

Note: DrawView uses the state pattern to accept input and animate, among other things. The state pattern is covered in the previous chapter.

All this theory here may sound a bit complex, but essentially DrawView will forward calls to add new lines or points to its currentState. If the currentState is AcceptInputState, the new lines and points will be added. If not, the calls will be ignored.

Okay, that’s enough theory!

Open DrawViewState.swift and add the following to the end of the file:

// MARK: - DrawViewDelegate
extension DrawViewState: DrawViewDelegate {
  public func drawView(_ source: DrawView,
                       didAddLine line: LineShape) { }

  public func drawView(_ source: DrawView,
                       didAddPoint point: CGPoint) { }
}

You made DrawViewState conform to DrawViewDelegate and provide empty implementations for both required methods. As a result, if the DrawViewState isn’t currently AcceptInputState, then these calls won’t do anything.

Next, open AcceptInputState.swift and add the following to the end of the file:

// MARK: - DrawViewDelegate
extension AcceptInputState {

  public override func drawView(_ source: DrawView,
                                didAddLine line: LineShape) {
    let newLine = line.copy() as LineShape
    addLine(newLine)
  }

  public override func drawView(_ source: DrawView,
                                didAddPoint point: CGPoint) {
    addPoint(point)
  }
}

Within drawView(_:didAddLine:), you create a newLine by copying the passed-in line and then call addLine to add it. You’re required to copy the line in order to have it displayed on both the original DrawView and this DrawView itself.

Within drawView(_:didAddPoint:), you simply call addPoint(_:) to add the point. Since CGPoint is a struct, which uses value semantics, it’s copied automatically.

You next need to make DrawView itself conform to DrawViewDelegate. Open DrawView.swift and add this to the end of the file:

// MARK: - DrawViewDelegate
extension DrawView: DrawViewDelegate {

  public func drawView(_ source: DrawView,
                       didAddLine line: LineShape) {
    currentState.drawView(source, didAddLine: line)
  }

  public func drawView(_ source: DrawView,
                       didAddPoint point: CGPoint) {
    currentState.drawView(source, didAddPoint: point)
  }
}

You simply pass the call through to the currentState.

You’re almost ready to try this out! The last thing you need to do is actually register the “mirror” DrawViews as delegates of the input DrawView.

Open ViewController.swift and add the following after the existing properties:

// MARK: - View Lifecycle
public override func viewDidLoad() {
  super.viewDidLoad()
  mirrorDrawViews.forEach {
    inputDrawView.addDelegate($0)
  }
}

You simply iterate through each mirrorDrawView and add them as delegates to inputDrawView. Build and run, and try drawing into the top-left draw view. Each of the other views should now be updated in real time as you draw!

Key points

You learned about the multicast delegate pattern in this chapter. Here are its key points:

  • The multicast delegate pattern allows you to create one-to-many delegate relationships. It involves four types: an object needing a delegate, a delegate protocol, delegates, and a multicast delegate.

  • An object needing a delegate has one or more delegates; the delegate protocol defines the methods a delegate should implement; the delegates implement the delegate protocol; and the multicast delegate is a helper class for holding onto and notifying the delegates.

  • Swift doesn’t provide a multicast delegate object for you. However, it’s easy to implement your own to support this pattern.

Mirror Pad is really functional now! However, there’s no way to share your amazing creations with the world… yet!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.