Event Bus and Observer Pattern in Swift (iOS/macOS)
In iOS and macOS development, there is often a need to organize loosely coupled interaction between application components. The Observer pattern and its implementation via an event bus (Event Bus) is a powerful tool for creating flexible and scalable architecture. In this article, we will analyze how to implement an Event Bus in Swift, compare it with Apple's built-in mechanisms (NotificationCenter, Combine), and show practical examples for iOS/macOS.
What is Event Bus and the Observer Pattern?
The Observer pattern defines a one-to-many relationship between objects: when one object (the publisher) changes its state, all dependent objects (subscribers) are automatically notified.
Event Bus is a centralized mechanism that allows any component to send events (publish) and receive them (subscribe) without knowing about each other directly. This implements the principle of loose coupling.
In the Apple ecosystem, there are already built-in solutions:
- NotificationCenter — a classic implementation of Observer, but with limitations in typing and safety.
- Combine — a modern framework for reactive programming, providing powerful Publisher/Subscriber capabilities.
However, a custom Event Bus provides full control and can be more easily integrated into legacy projects.
Implementing a Simple Event Bus in Swift
Let's create a universal event bus using generics and weak references to prevent memory leaks.
import Foundation
// Protocol for eventsprotocol Event { }
// Event Bus implementationfinal class EventBus { static let shared = EventBus() private var observers = [ObjectIdentifier: [WeakObserver]]() private init() { } // Subscribe to an event func subscribe<T: Event>(observer: AnyObject, handler: @escaping (T) -> Void) { let id = ObjectIdentifier(T.self) let weakRef = WeakObserver(observer: observer, handler: { event in guard let castedEvent = event as? T else { return } handler(castedEvent) }) if observers[id] != nil { observers[id]?.append(weakRef) } else { observers[id] = [weakRef] } // Clean up dead references cleanDeadObservers(for: id) } // Publish an event func publish<T: Event>(_ event: T) { let id = ObjectIdentifier(T.self) guard let observerList = observers[id] else { return } for weakObserver in observerList { weakObserver.handler?(event) } // Remove deallocated objects cleanDeadObservers(for: id) } // Unsubscribe func unsubscribe(observer: AnyObject, eventType: Event.Type) { let id = ObjectIdentifier(eventType) observers[id]?.removeAll { $0.observer === observer } } private func cleanDeadObservers(for id: ObjectIdentifier) { observers[id]?.removeAll { $0.observer == nil } }}
// Helper class for weak referenceprivate class WeakObserver { weak var observer: AnyObject? var handler: ((Event) -> Void)? init(observer: AnyObject, handler: @escaping (Event) -> Void) { self.observer = observer self.handler = handler }}Usage Example
Define custom events:
struct UserLoggedInEvent: Event { let userId: String let userName: String}
struct DataLoadedEvent: Event { let data: [String]}Usage in a ViewController:
class ProfileViewController: UIViewController { override func Also in library
Redis (ioredis) for JavaScript (Node.js): Complete Guide with Examples
Working with YAML in C++: yaml-cpp and rapidyaml libraries
Criterion for Rust: Complete Guide with Examples