Event Bus и паттерн Observer в Swift (iOS/macOS)
В разработке под iOS и macOS часто возникает необходимость организовать слабосвязанное взаимодействие между компонентами приложения. Паттерн Observer и его реализация в виде Event Bus (шины событий) — это мощный инструмент для решения этой задачи. В этой статье мы подробно разберём, как реализовать Event Bus на Swift, используя современные возможности языка (Generics, closures, weak references).
Что такое паттерн Observer?
Паттерн Observer (Наблюдатель) определяет отношение "один ко многим" между объектами. Когда один объект (издатель) изменяет своё состояние, все зависимые от него объекты (подписчики) автоматически уведомляются и обновляются. Это позволяет избежать жёсткой связанности и упрощает поддержку кода.
В Swift нативными реализациями Observer являются:
NotificationCenter— глобальная шина уведомлений, встроенная в Foundation.- Key-Value Observing (KVO) — механизм наблюдения за свойствами объектов.
- Combine framework (iOS 13+) — реактивный фреймворк с поддержкой издателей и подписчиков.
Однако часто требуется более лёгкая и типобезопасная реализация, особенно при работе с legacy-кодом или в проектах, где Combine недоступен.
Event Bus: простая и гибкая реализация
Event Bus — это центральный диспетчер событий, который позволяет подписываться на события и отправлять их без прямого знания о подписчиках. Мы реализуем его с использованием дженериков и слабых ссылок, чтобы избежать утечек памяти.
Базовая реализация Event Bus
import Foundation
// Протокол для всех событий
protocol Event { }
// Конкретные типы событий
struct UserLoggedInEvent: Event {
let userId: String
let userName: String
}
struct DataLoadedEvent: Event {
let data: [String]
}
// Класс Event Bus
final class EventBus {
static let shared = EventBus()
private init() {}
// Словарь для хранения подписчиков
private var subscribers = [ObjectIdentifier: [Any]]()
// Подписка на событие
func subscribe<T: Event>(observer: AnyObject, handler: @escaping (T) -> Void) {
let key = ObjectIdentifier(T.self)
let wrapped = Subscription(observer: observer, handler: handler as Any)
if subscribers[key] != nil {
subscribers[key]?.append(wrapped)
} else {
subscribers[key] = [wrapped]
}
}
// Отписка от всех событий для конкретного наблюдателя
func unsubscribe(observer: AnyObject) {
for key in subscribers.keys {
subscribers[key]?.removeAll(where: { subscription in
if let sub = subscription as? Subscription {
return sub.observer === observer
}
return false
})
}
}
// Публикация события
func publish<T: Event>(event: T) {
let key = ObjectIdentifier(T.self)
guard let subs = subscribers[key] else { return }
for subscription in subs {
if let sub = subscription as? Subscription {
if let handler = sub.handler as? (T) -> Void {
handler(event)
}
}
}
}
}
// Вспомогательная структура для хранения подписок
private struct Subscription {
weak var observer: AnyObject?
let handler: Any
init(observer: AnyObject, handler: Any) {
self.observer = observer
self.handler = handler
}
}
Использование Event Bus
// ViewController, который подписывается на события
class LoginViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Подписываемся на событие входа пользователя
EventBus.shared.subscribe(observer: self) { [weak self] (event: UserLoggedInEvent) in
print("Пользователь \(event.userName) вошёл в систему")
self?.updateUI(with: event)
}
}
private func updateUI(with event: UserLoggedInEvent) {
// Обновление интерфейса
}
deinit {
// Важно: отписываемся при уничтожении
EventBus.shared.unsubscribe(observer: self)
}
}
// Сервис, который генерирует события
class AuthService {
func login(userId: String, userName: String) {
// Логика входа
let event = UserLoggedInEvent(userId: userId, userName: userName)
EventBus.shared.publish(event: event)
}
}
Продвинутая версия с автоматической отпиской
Чтобы не забывать вызывать unsubscribe, можно реализовать автоматическую отписку с помощью специального объекта-токена:
// Токен для автоматической отписки
class EventSubscriptionToken {
let unsubscribeClosure: () -> Void
init(unsubscribeClosure: @escaping () -> Void) {
self.unsubscribeClosure = unsubscribeClosure
}
deinit {
unsubscribeClosure()
}
}
// Расширение EventBus с поддержкой токенов
extension EventBus {
@discardableResult
func subscribeWithToken<T: Event>(observer: Any