Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

16. Multicast Delegate Pattern
Written by Joshua Greene

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

// 1
public class MulticastDelegate<ProtocolType> {

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

    weak var delegate: AnyObject?

    init(_ delegate: AnyObject) {
      self.delegate = delegate
    }
  }
}
// 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)
  }
}
// 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)
}
public func invokeDelegates(_ closure: (ProtocolType) -> ()) {
  delegates.forEach { closure($0) }
}
// MARK: - Delegate Protocol
public protocol EmergencyResponding {
  func notifyFire(at location: String)
  func notifyCarCrash(at location: String)
}
// 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)
  }
}
// MARK: - Delegating Object
public class DispatchSystem {
  let multicastDelegate =
    MulticastDelegate<EmergencyResponding>()
}
// MARK: - Example
let dispatch = DispatchSystem()
var policeStation: PoliceStation! = PoliceStation()
var fireStation: FireStation! = FireStation()

dispatch.multicastDelegate.addDelegate(policeStation)
dispatch.multicastDelegate.addDelegate(fireStation)
dispatch.multicastDelegate.invokeDelegates {
  $0.notifyFire(at: "Ray’s house!")
}
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!")
}
Police were notified about a car crash at Ray's garage!

What should you be careful about?

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

Tutorial project

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

@objc public protocol DrawViewDelegate: class {
  func drawView(_ source: DrawView, didAddLine line: LineShape)
  func drawView(_ source: DrawView, didAddPoint point: CGPoint)
}
// MARK: - Delegate Management
public let multicastDelegate =
  MulticastDelegate<DrawViewDelegate>()

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

public func removeDelegate(_ delegate: DrawViewDelegate) {
  multicastDelegate.removeDelegate(delegate)
}
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)
}
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)
}
// MARK: - DrawViewDelegate
extension DrawViewState: DrawViewDelegate {
  public func drawView(_ source: DrawView,
                       didAddLine line: LineShape) { }

  public func drawView(_ source: DrawView,
                       didAddPoint point: CGPoint) { }
}
// 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)
  }
}
// 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)
  }
}
// MARK: - View Lifecycle
public override func viewDidLoad() {
  super.viewDidLoad()
  mirrorDrawViews.forEach {
    inputDrawView.addDelegate($0)
  }
}

Key points

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

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now