Software Design

Swift Pub-Sub Pattern vs. Observer Pattern (Event Bus Guide)

When building an iOS app, you eventually hit a familiar roadblock.

4 min read
Cover image for Swift Pub-Sub Pattern vs. Observer Pattern (Event Bus Guide)

When building an iOS app, you eventually hit a familiar roadblock.

When something happens on screen A and a distant screen B needs to know, you wonder how to connect them.

Passing delegates around can easily tangle your code into spaghetti. That is when Swift Pub-Sub and an event bus come in handy.

Today, I’ll clarify how Swift Pub-Sub differs from the Observer pattern and what an event bus actually is.

In short, the Observer pattern has the subject know its subscribers directly, while Pub-Sub inserts a broker—the event bus—to decouple them.

Once you understand that sentence, the rest falls into place. Let’s walk through it with code.


Let’s start with the Observer pattern

The Observer pattern directly connects the observed subject and its observers.

When the subject changes, it sends notifications directly to its registered observers.

If you’ve worked with iOS, you’ve probably used this already. Typical examples include NotificationCenter, Combine’s @Published, and KVO (Key-Value Observing).

The key point is that the subject maintains its own list of subscribers.

// The subject directly manages its subscribers
class Subject {
    private var observers: [Observer] = []
    func add(_ o: Observer) { observers.append(o) }
    func notify() { observers.forEach { $0.update() } }
}

The structure is simple and intuitive, making it a good fit for one-to-many relationships.

However, requiring the subject to know about its observers can become burdensome as the system grows.


How is Swift Pub-Sub different?

The Pub-Sub pattern takes this one step further.

It inserts a broker between the publisher and subscriber. This broker is the event bus, or message broker.

The publisher simply sends “something happened” to the bus. It does not know who receives it.

The subscriber only registers with the bus: “Tell me when this event occurs.” It does not know who sent it.

The key is that neither side knows the other at all. This is called loose coupling, or decoupling.

Direct connection vs. connection through a bus, illustrated
Direct connection vs. connection through a bus, illustrated
// The event bus brokers communication between publishers and subscribers
enum AppEvent { case userLoggedIn(id: String) }

final class EventBus {
    static let shared = EventBus()
    private var handlers: [(AppEvent) -> Void] = []
    func subscribe(_ h: @escaping (AppEvent) -> Void) { handlers.append(h) }
    func publish(_ e: AppEvent) { handlers.forEach { $0(e) } }
}

For the publisher, it takes just one line: EventBus.shared.publish(.userLoggedIn(id: "123")).

The login screen does not even need to know that the home screen exists.

Publishing with one EventBus line—that simple
Publishing with one EventBus line—that simple

Observer vs. Pub-Sub: A quick comparison

They sound similar, so here is a table highlighting the differences.

Category Observer pattern Pub-Sub pattern
Broker None (direct connection) Present (event bus)
Coupling The subject knows its subscribers Neither knows the other (loose)
Relationship Usually 1:N N:N is possible
iOS example KVO, @Published NotificationCenter, event bus
Best suited for Observing a specific object’s state Communication between distant modules

Interestingly, NotificationCenter is actually closer to Pub-Sub.

Although it is called “Notification,” publishers and subscribers meet only through the NotificationCenter bus. They do not reference each other directly.

So memorizing “Observer pattern = NotificationCenter” is slightly misleading. Conceptually, it is closer to Pub-Sub.


So when should you use each one?

Here is the rule of thumb I use in practice.

If you need to observe one object’s state changes nearby, an Observer-style approach is convenient. Combine’s @Published and SwiftUI’s @Observable are a perfect fit.

Conversely, for major events across distant modules or throughout the app—login, payment completion, or network loss—an event bus is much cleaner.

That said, an event bus is not a silver bullet.

Overusing it makes the flow hard to trace: “Where is this event even coming from?” You gain decoupling at the cost of some visibility.

I therefore use Observer (Combine) for in-screen logic and reserve the event bus for major events crossing screens and modules.

Routing only major events through the bus keeps the flow clean
Routing only major events through the bus keeps the flow clean

Q. If I use Combine, do I still need an event bus?

No. You can build a simple event bus yourself with just one Combine PassthroughSubject. The tools overlap, but the concepts are not interchangeable.

Q. Is Pub-Sub always the better pattern?

Not necessarily. When the relationship is close and decoupling is unnecessary, Observer is easier to read and debug.


Observer means a direct connection; Pub-Sub means a connection through a broker. That is the whole difference.

They are not competing patterns, but tools chosen for the situation. When designing your next screen, first consider how much the two sides need to know about each other; the right pattern will quickly become clear.