Skip to content

Multicast Delegate ​

A Multicast Delegate sends the same callback to several recipients. A normal delegate has one owner; this pattern suits several peers that all need an event.

iOS use case ​

An upload manager updates a progress screen and an app-wide badge. Both should receive progress, but neither should own the manager or keep the other alive.

swift
import Foundation

@MainActor
protocol UploadProgressDelegate: AnyObject {
    func uploadDidProgress(to fraction: Double)
}

@MainActor
final class UploadProgressHub {
    private let listeners = NSHashTable<AnyObject>.weakObjects()

    func add(_ listener: any UploadProgressDelegate) {
        listeners.add(listener)
    }

    func remove(_ listener: any UploadProgressDelegate) {
        listeners.remove(listener)
    }

    func publish(_ fraction: Double) {
        for object in listeners.allObjects {
            (object as? any UploadProgressDelegate)?.uploadDidProgress(to: fraction)
        }
    }
}

Weak storage prevents the hub from extending screen lifetimes. The main actor makes callbacks safe for UIKit and SwiftUI presentation code. A screen registers when it needs progress and may remove itself earlier than deallocation.

Production decisions ​

If a listener needs the current progress immediately after subscribing, store the latest value and replay it on add. Decide whether listener order matters; this implementation does not promise an order. Combine publishers, AsyncStream, or Observation may fit better when the event is really state that can be subscribed to declaratively.

How delivery and ownership work ​

UploadProgressHub keeps listeners in NSHashTable.weakObjects(), so registering a screen does not retain it. publish(_:) takes a current snapshot of live objects, casts each to UploadProgressDelegate, and calls its method. The protocol and hub are main-actor isolated because progress UI is updated on the main actor. Weak storage also means a listener can disappear between events without explicit cleanup.

From example to a real upload ​

An upload service receives byte counts from URLSession and calculates a fraction. It passes that fraction to the main-actor hub. A progress controller and a tab badge can both register; neither knows about the other. When a screen stops showing progress, remove it explicitly so it does not receive irrelevant events while still retained elsewhere. A newly registered listener may need the latest progress immediately, so store and replay that value if the UX requires it.

Failure modes and verification ​

Test two listeners, removal, deallocation, and a listener joining midway. Do not rely on notification order from a weak hash table. If subscribers need buffering, replay, or async iteration, AsyncStream or Combine may be a better abstraction. Multicast Delegate is for one event sent to several recipients; ordinary Delegate is for one owner handling a child's callbacks.

All patterns

Released under the MIT License.