Почему вашему Swift-приложению нужна event-driven архитектура?
Представьте, что вы дирижёр оркестра. Каждый музыкант ждёт своей партии, чтобы вступить в нужный момент. Если бы скрипачи играли всё сразу — получился бы хаос. Примерно так же работает event-driven архитектура на Swift: каждый модуль реагирует только на определённые события, а не дёргает другие модули за ниточки напрямую.
Когда я впервые столкнулся с событийно-ориентированным программированием Swift в коммерческом проекте, я понял, насколько это мощный инструмент. Приложение для доставки еды: пользователь нажал кнопку «Заказать» — это событие. Корзина обновилась — событие. Пришёл push с курьером — событие. Каждый модуль (UI, сеть, локальная БД) просто подписывается на нужные события и работает независимо. Это и есть event-driven архитектура.
В этой статье мы разберём три подхода к реализации архитектуры событий Swift: классические делегаты, NotificationCenter и кастомную шину событий. Вы увидите реальный код, поймёте, когда что использовать, и сможете сразу применить в своём проекте. Поехали!
Основы event-driven подхода на Swift
Что такое событие в контексте Swift?
Событие — это сигнал о том, что что-то произошло. Это может быть:
- Пользовательский ввод (кнопка нажата, текст изменён)
- Изменение данных (пришли данные с сервера, обновилась БД)
- Системные уведомления (приложение ушло в фон, клавиатура появилась)
Главный принцип event-driven архитектуры Swift — слабая связанность (loose coupling). Отправитель события не знает, кто его обработает. Получатель не знает, кто его отправил. Они общаются через шину событий — это делает код гибким и тестируемым.
Паттерн Observer — фундамент событий
Паттерн Observer Swift лежит в основе любой event-driven системы. У нас есть один объект-издатель (Publisher) и множество подписчиков (Subscribers). Когда издатель меняет состояние, он уведомляет всех подписчиков.
В Swift есть несколько встроенных механизмов для этого:
NotificationCenter— глобальная шина событий в iOS SDK- Делегаты — один-на-один, но с контролем
Combine— реактивный фреймворк от Apple (iOS 13+)- Кастомные реализации — полный контроль над поведением
Давайте разберём каждый подход с примерами.
Способ 1. Делегаты: классика жанра
Делегаты Swift — это самый простой способ реализовать событие. Он подходит для отношений «один-на-один», когда один объект сообщает другому о своём состоянии.
Пример: представьте, что у нас есть загрузчик изображений, который должен уведомить контроллер о завершении.
// 1. Определяем протокол делегата
protocol ImageLoaderDelegate: AnyObject {
func imageLoader(_ loader: ImageLoader, didLoad image: UIImage)
func imageLoader(_ loader: ImageLoader, didFailWith error: Error)
}
// 2. Класс-издатель
class ImageLoader {
weak var delegate: ImageLoaderDelegate?
func loadImage(from url: URL) {
URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
DispatchQueue.main.async {
if let error = error {
self?.delegate?.imageLoader(self!, didFailWith: error)
} else if let data = data, let image = UIImage(data: data) {
self?.delegate?.imageLoader(self!, didLoad: image)
}
}
}.resume()
}
}
// 3. Контроллер-подписчик
class ViewController: UIViewController, ImageLoaderDelegate {
let loader = ImageLoader()
@IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
loader.delegate = self
loader.loadImage(from: URL(string: "https://example.com/image.jpg")!)
}
func imageLoader(_ loader: ImageLoader, didLoad image: UIImage) {
imageView.image = image
}
func imageLoader(_ loader: ImageLoader, didFailWith error: Error) {
print("Ошибка загрузки: \(error.localizedDescription)")
}
}
Плюсы: типобезопасность, явная связь, легко отлаживать. Минусы: только один делегат на объект, сложно масштабировать на много подписчиков.
Способ 2. NotificationCenter: глобальная шина из коробки
NotificationCenter Swift — это встроенная система уведомлений. Она позволяет отправлять события глобально, и любой объект может на них подписаться. Идеально для событий, которые нужны многим частям приложения одновременно.
Пример: пользователь успешно залогинился — нужно обновить меню, скрыть экран логина, показать приветствие.
// 1. Определяем имя уведомления (лучше вынести в константы)
extension Notification.Name {
static let userDidLogin = Notification.Name("userDidLogin")
}
// 2. Отправляем событие (из сервиса авторизации)
class AuthService {
func login(username: String, password: String) {
// ... логика авторизации
NotificationCenter.default.post(name: .userDidLogin, object: nil, userInfo: ["username": username])
}
}
// 3. Подписываемся на событие (в любом контроллере)
class ProfileViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(handleLogin),
name: .userDidLogin,
object: nil
)
}
@objc func handleLogin(_ notification: Notification) {
if let username = notification.userInfo?["username"] as? String {
print("Привет, \(username)!")
// обновляем UI
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
Важно! Всегда отписывайтесь от уведомлений в deinit, иначе получите краш при обращении к уже уничтоженному объекту. Это классическая ошибка новичков в событийно-ориентированном программировании Swift.
Способ 3. Кастомная шина событий на Swift
Когда NotificationCenter становится слишком глобальным, а делегаты — слишком тесными, приходит время для собственной шины событий Swift. Это даёт полный контроль: можно фильтровать события, добавлять строгую типизацию, легко тестировать.
Я покажу простую, но мощную реализацию с использованием дженериков.
// 1. Протокол для любого события
protocol Event {}
// 2. Конкретные события
struct UserLoggedInEvent: Event {
let userId: String
let username: String
}
struct CartUpdatedEvent: Event {
let itemCount: Int
}
// 3. Шина событий
final class EventBus {
static let shared = EventBus()
private var handlers: [String: [(Any) -> Void]] = [:]
private init() {}
// Подписка
func subscribe<T: Event>(_ eventType: T.Type