Какие основные подходы к управлению глобальным состоянием в iOS-приложении?

Ответ

Выбор подхода зависит от сложности приложения, требований к тестируемости и масштабируемости.

1. Для простых сценариев и SwiftUI:

  • @EnvironmentObject — удобная инъекция состояния в дерево представлений.
  • @AppStorage / @SceneStorage — хранение простых значений (String, Int, Bool) с автоматической синхронизацией в UserDefaults.

2. Для более сложных приложений:

  • Сочетание ObservableObject (модель) + @StateObject / @ObservedObject (вью) — реактивное управление состоянием с помощью Combine.
  • Координаторы / Роутеры — отделение логики навигации и потока данных от UI.

3. Для масштабируемых проектов (архитектурные паттерны):

  • Redux-подобные решения (The Composable Architecture - TCA, ReSwift) — централизованное хранилище с односторонним потоком данных (State -> View -> Action -> Reducer). Гарантирует предсказуемость и упрощает тестирование.
  • Service Locator / DI-контейнеры — управление зависимостями вместо жестко закодированных синглтонов.

4. Чего следует избегать и почему:

  • Глобальные синглтоны (static let shared) — создают скрытые зависимости, усложняют модульное тестирование и повторное использование кода. Лучше использовать инъекцию зависимостей.
  • Массивное использование NotificationCenter для передачи данных — приводит к хрупкому коду, где связи между компонентами неявны и сложно отслеживаемы.

Пример DI через инициализатор:

class DataService {
    func fetch() { /* ... */ }
}

class ViewModel {
    let service: DataService
    init(service: DataService) { // Зависимость внедряется извне
        self.service = service
    }
}
// Тестирование упрощается:
let mockService = MockDataService()
let viewModel = ViewModel(service: mockService)

Ответ 18+ 🔞

А, ну вот, опять про эту дичь с передачей данных! Слушай, тут всё зависит от того, насколько ты готов свою жопу рвать. Простое приложение — одно, а если ты замахнулся на что-то монструозное, то тут уже, блядь, надо думать головой, а не жопой.

1. Для простенького, чтоб не париться:

  • @EnvironmentObject — это как халявная инъекция прямо в дерево вьюшек. Удобно, но если накосячить с областью видимости — пиши пропало, нихуя не найдёшь, где оно потерялось.
  • @AppStorage / @SceneStorage — для сохранения какой-нибудь хуйни вроде настроек. Типа, «запомнить логин» или «тёмная тема». Автоматически пишется в UserDefaults, ебушки-воробушки.

2. Если уже не детский сад, а что-то посерьёзнее:

  • Комбинация ObservableObject (модель) + @StateObject / @ObservedObject (вью) — классика жанра с Combine. Реактивное управление состоянием, но если не следить, то можно такую кашу из подписок замутить, что сам потом будешь плакать.
  • Координаторы / Роутеры — это когда ты выносишь логику навигации и поток данных подальше от UI. Чтобы вьюхи не знали, куда и как их швыряют. Умно, но требует дисциплины, блядь.

3. Для проектов, где масштаб уже овердохуища (архитектурные паттерны):

  • Redux-подобные штуки (The Composable Architecture - TCA, ReSwift) — тут у тебя централизованное хранилище и односторонний поток данных: State -> View -> Action -> Reducer. Предсказуемость на максимум, тестирование упрощается в разы, но порог входа — ёперный театр, учить надо.
  • Service Locator / DI-контейнеры — вместо этих ебучих глобальных синглтонов, которые все ненавидят, но все используют. Управляешь зависимостями цивилизованно.

4. Чего делать НЕ НАДО, иначе будет пиздец:

  • Глобальные синглтоны (static let shared) — это, блядь, классическая ловушка для распиздяев. Создают скрытые зависимости, тестировать нихуя невозможно, повторно использовать код — забыть. Лучше нормальную инъекцию зависимостей делать.
  • Засирать всё NotificationCenter для передачи данных — вот это, сука, самый верный путь к хрупкому коду, где связи между компонентами неявные, и когда что-то ломается, ты тратишь три дня, чтобы понять, откуда прилетело это ебаное уведомление.

Вот, смотри, как можно по-человечески зависимости внедрять:

class DataService {
    func fetch() { /* ... */ }
}

class ViewModel {
    let service: DataService
    init(service: DataService) { // Зависимость не из воздуха, а принесли и всунули
        self.service = service
    }
}
// А тестируется это теперь — красота!
let mockService = MockDataService()
let viewModel = ViewModel(service: mockService)

Вот и вся магия. Главное — не выдумывать велосипед и не херачить костыли там, где можно сделать нормально.