In modern iOS applications, data is constantly changing. A user logs in, a network request finishes, or a background sync completes. When these events happen, multiple parts of your application—like the UI, analytics trackers, and local databases—need to react immediately.

If the object responsible for the data explicitly tells every other object to update, your app becomes a tightly coupled, unmaintainable mess.

This is exactly what the Observer design pattern prevents. It defines a one-to-many subscription mechanism so that when one object changes state, all its dependents are notified automatically, without the objects needing to know about each other.


The Violation: Hardcoded Dependencies

Imagine a music streaming app. We have an AudioPlayer that manages the current song. When the song changes, the MiniPlayerView, the FullScreenPlayerView, and the LockScreenManager all need to update their UI.

❌ The Anti-Pattern

class MiniPlayerView {
    func updateUI(with song: String) { print("MiniPlayer updating to \(song)") }
}

class FullScreenPlayerView {
    func refresh(songTitle: String) { print("FullScreen updating to \(songTitle)") }
}

class LockScreenManager {
    func syncNowPlaying(song: String) { print("LockScreen syncing with \(song)") }
}

class AudioPlayer {
    // Tightly coupled dependencies!
    var miniPlayer: MiniPlayerView?
    var fullScreenPlayer: FullScreenPlayerView?
    var lockScreen: LockScreenManager?
    
    var currentSong: String = "" {
        didSet {
            notifyDependencies()
        }
    }
    
    private func notifyDependencies() {
        // The AudioPlayer knows way too much about the outside world.
        miniPlayer?.updateUI(with: currentSong)
        fullScreenPlayer?.refresh(songTitle: currentSong)
        lockScreen?.syncNowPlaying(song: currentSong)
    }
}

Why this fails: * Violates the Open/Closed Principle: If we add a LyricsViewController next week, we have to modify the AudioPlayer to hold a reference to it and call its update method.

  • Retain Cycles & Memory Leaks: Managing references to ViewControllers inside a singleton or global manager often leads to memory leaks if you aren’t perfectly managing weak references everywhere.

The Fix: The Classic Observer Pattern

We fix this by flipping the relationship. The AudioPlayer (the Subject) will no longer care who is listening. It will simply maintain a list of abstract “Observers” and broadcast changes to them.

✅ Step 1: Define the Observer Protocol

First, we create a protocol that any interested class can adopt. We restrict it to AnyObject so we can use weak references to prevent memory leaks.

protocol AudioPlayerObserver: AnyObject {
    func audioPlayerDidUpdateSong(to song: String)
}

✅ Step 2: Implement the Subject (AudioPlayer)

Since Swift arrays hold strong references, we need a small wrapper to hold weak references to our observers. This prevents the AudioPlayer from keeping UI components alive after they are dismissed.

struct WeakObserver {
    weak var value: AudioPlayerObserver?
}

class AudioPlayer {
    private var observers: [WeakObserver] = []
    
    var currentSong: String = "" {
        didSet {
            notifyObservers()
        }
    }
    
    // Subscribe
    func addObserver(_ observer: AudioPlayerObserver) {
        // Clean up any dead references first
        observers.removeAll(where: { $0.value == nil })
        observers.append(WeakObserver(value: observer))
    }
    
    // Unsubscribe
    func removeObserver(_ observer: AudioPlayerObserver) {
        observers.removeAll(where: { $0.value === observer || $0.value == nil })
    }
    
    // Broadcast
    private func notifyObservers() {
        for observer in observers {
            observer.value?.audioPlayerDidUpdateSong(to: currentSong)
        }
    }
}

✅ Step 3: Implement the Observers

Now, our views simply conform to the protocol and register themselves with the player.

class MiniPlayerView: AudioPlayerObserver {
    init(player: AudioPlayer) {
        player.addObserver(self)
    }
    
    func audioPlayerDidUpdateSong(to song: String) {
        print("MiniPlayer smoothly transitioned to: \(song)")
    }
}

// Usage:
let player = AudioPlayer()
let miniPlayer = MiniPlayerView(player: player)

// The player doesn't know about the miniPlayer directly, but it still gets the update!
player.currentSong = "Bohemian Rhapsody" 

Native Observer Patterns in Swift

While the classic Gang of Four pattern above is essential to understand, Apple provides native tools built directly into the iOS SDK to achieve this exact same behavior.

ApproachBest ForCharacteristics
Classic ProtocolStrict type safety, clean architectureRequires manual weak array management. Great for internal module logic.
NotificationCenterGlobal, cross-app eventsString-based, loosely typed. Easy to use but hard to debug “spaghetti code.”
KVO (Key-Value Observing)Interacting with old Objective-C APIsClunky syntax in modern Swift. Rarely used for new code.
Combine / @PublishedModern Swift apps, SwiftUINative, highly powerful, thread-safe, and declarative. The modern standard.

The Modern Fix: Using Combine

If you are building a modern iOS app, you should use Apple’s Combine framework (or SwiftUI’s ObservableObject) instead of writing custom observer arrays. It handles the weak references and memory management automatically.

import Combine

class ModernAudioPlayer {
    // The Subject
    @Published var currentSong: String = ""
}

class ModernMiniPlayer {
    private var cancellables = Set<AnyCancellable>()
    
    init(player: ModernAudioPlayer) {
        // The Subscription (Observer)
        player.$currentSong
            .dropFirst() // Ignore the initial empty string
            .sink { newSong in
                print("Combine MiniPlayer updated to: \(newSong)")
            }
            .store(in: &cancellables) // Auto-cancels when MiniPlayer is deallocated
    }
}

Where to Use the Observer Pattern

  1. One-to-Many Relationships: When one state change needs to trigger updates in multiple completely unrelated parts of your app (e.g., User logs out -> UI clears, DB clears, analytics sends event).
  2. Decoupling Core Logic from UI: Your business models shouldn’t know anything about UIView or UIViewController. Observers allow the model to announce changes without knowing who is drawing the screen.
  3. Cross-Module Communication: When a framework (like a Bluetooth scanner) needs to notify the main app about hardware events without tight coupling.

Where NOT to Use the Observer Pattern

  • One-to-One Relationships: If only one object ever cares about the event, use a simple Delegate pattern or a Completion Handler closure instead. Observers are overkill for 1:1 communication.
  • Sequential Logic: If Step B must happen strictly after Step A, and Step C strictly after Step B, do not use Observers. Observers do not guarantee execution order. Use asynchronous flows (async/await) or pipelines for sequential logic.
  • “Notification Hell”: If everything observes everything, tracking down the source of a bug becomes a nightmare. (e.g., A changes B, B changes C, C changes A, causing an infinite loop).

Final Thoughts

The Observer pattern is the backbone of reactive programming. Whether you implement it manually via protocols to understand the mechanics, use NotificationCenter for global broadcasts, or leverage Combine for elegant data streams, mastering this pattern is non-negotiable for building scalable iOS applications.