Mediator Design Pattern in Swift


Greetings, traveler!

We continue to discuss Design Patterns. It is the turn of another Behavioral Design Pattern, the Mediator.

At first glance it feels similar to Observer. Or even to a simple service object. The difference only becomes clear when the system starts growing and objects begin talking to each other in too many directions.

Mediator exists for one reason: to stop objects from knowing too much about each other.

Let’s build a real example and see when it actually helps.

The problem: too many direct connections

Imagine a simple settings screen with three components:

  • WiFiSwitch
  • CellularSwitch
  • AirplaneModeSwitch

There is business logic behind them:

  • Enabling Airplane Mode must disable WiFi and Cellular.
  • Enabling WiFi while Airplane Mode is on must disable Airplane Mode.
  • Disabling Cellular might trigger a warning label somewhere else.

If each component talks directly to the others, the dependencies quickly become messy.

final class WiFiSwitch {
    var airplaneMode: AirplaneModeSwitch?
    
    func turnOn() {
        airplaneMode?.turnOff()
    }
}

Now multiply this across three or four components. Each one keeps references to others. Logic spreads everywhere. Changing one rule means touching multiple classes.

This is where Mediator makes sense.

The idea behind mediator

Instead of components communicating directly, they send events to a central object. That object decides what should happen next.

Components no longer know about each other. They only know about the mediator.

Let’s model it properly.

Defining the mediator protocol

protocol SettingsMediator: AnyObject {
    func notify(sender: AnyObject, event: SettingsEvent)
}

enum SettingsEvent {
    case wifiOn
    case wifiOff
    case cellularOn
    case cellularOff
    case airplaneOn
    case airplaneOff
}

The mediator does not expose specific methods like enableWiFi or disableCellular. It receives events and coordinates behavior.

That keeps components dumb. The mediator owns the logic.

Building the concrete mediator

final class DefaultSettingsMediator: SettingsMediator {
    
    private var wifi: WiFiSwitch?
    private var cellular: CellularSwitch?
    private var airplane: AirplaneModeSwitch?
    
    func register(
        wifi: WiFiSwitch,
        cellular: CellularSwitch,
        airplane: AirplaneModeSwitch
    ) {
        self.wifi = wifi
        self.cellular = cellular
        self.airplane = airplane
    }
    
    func notify(sender: AnyObject, event: SettingsEvent) {
        switch event {
        case .airplaneOn:
            wifi?.set(enabled: false)
            cellular?.set(enabled: false)
            
        case .wifiOn:
            airplane?.set(enabled: false)
            
        case .cellularOn:
            airplane?.set(enabled: false)
            
        default:
            break
        }
    }
}

All coordination logic lives here. If the rules change, you update one place.

Updating the components

Components now depend only on the mediator.

final class WiFiSwitch {
    
    private weak var mediator: SettingsMediator?
    private(set) var isEnabled = false
    
    init(mediator: SettingsMediator) {
        self.mediator = mediator
    }
    
    func toggle() {
        isEnabled.toggle()
        
        if isEnabled {
            mediator?.notify(sender: self, event: .wifiOn)
        } else {
            mediator?.notify(sender: self, event: .wifiOff)
        }
    }
    
    func set(enabled: Bool) {
        isEnabled = enabled
    }
}

The switch does not know about Cellular or Airplane Mode anymore. It simply reports state changes.

The same structure applies to other switches.

Wiring everything together

let mediator = DefaultSettingsMediator()

let wifi = WiFiSwitch(mediator: mediator)
let cellular = CellularSwitch(mediator: mediator)
let airplane = AirplaneModeSwitch(mediator: mediator)

mediator.register(
    wifi: wifi,
    cellular: cellular,
    airplane: airplane
)

Now interactions flow through one center.

Why this is not observer

Observer broadcasts changes to subscribers. Mediator coordinates behavior between peers.

In Observer:

  • One object publishes
  • Many observe

In Mediator:

  • Multiple objects interact
  • One object decides how they affect each other

If your logic is just “tell everyone something changed,” you probably need Observer.

If your logic sounds like “when A does this, B and C must react in a specific way,” that is mediator territory.

When mediator is useful in iOS projects

You will see this pattern in:

  • Complex forms where fields depend on each other
  • Onboarding flows with conditional steps
  • Coordinators managing screen interactions
  • Feature modules that must stay isolated

In SwiftUI, this often appears as a ViewModel that coordinates smaller sub-view models. You may not call it Mediator, but the structure is the same.

When not to use it

If there are only two objects interacting, mediator adds ceremony.

If the interaction is simple and stable, direct references are fine.

Mediator helps when rules change often and connections multiply. It is not a default solution, but a response to growing complexity.

Conclusion

Mediator reorganizes dependencies. Instead of many objects depending on each other, they depend on one coordinator. That shift alone can make a system easier to reason about.

The moment you notice objects passing messages in multiple directions, stop and sketch the dependency graph. If it looks like spaghetti, a mediator may be the simplest way to untangle it.

And the best part is that the pattern in Swift is lightweight. Just protocols and discipline.

Now, let’s move on to the next design pattern, the Memento pattern.