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
weakreferences 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.
| Approach | Best For | Characteristics |
|---|---|---|
| Classic Protocol | Strict type safety, clean architecture | Requires manual weak array management. Great for internal module logic. |
| NotificationCenter | Global, cross-app events | String-based, loosely typed. Easy to use but hard to debug “spaghetti code.” |
| KVO (Key-Value Observing) | Interacting with old Objective-C APIs | Clunky syntax in modern Swift. Rarely used for new code. |
| Combine / @Published | Modern Swift apps, SwiftUI | Native, 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
- 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).
- Decoupling Core Logic from UI: Your business models shouldn’t know anything about
UIVieworUIViewController. Observers allow the model to announce changes without knowing who is drawing the screen. - 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
Delegatepattern or aCompletion Handlerclosure 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.