Event-driven архитектура на Swift: превращаем код в оркестр событий

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему вашему 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                

Блоги

Рекомендаций по книгам