Настройка Dependency Injection с Factory в iOS-приложении (SwiftUI)
Factory — DI-библиотека Майкла Лонга, написанная специально под Swift и SwiftUI. В отличие от Swinject, она не использует string-based registration и не кидает runtime-краш при незарегистрированной зависимости. Всё разрешается на уровне типов в compile time, а ошибки конфигурации видны сразу в Xcode.
Почему Factory вместо Swinject для SwiftUI
Swinject создавался под UIKit и Objective-C runtime. В SwiftUI-проекте он работает, но @Environment и @StateObject создают конкуренцию за управление жизненным циклом объектов — появляются вопросы, кто владелец ViewModel. Factory использует @Injected property wrapper и интегрируется с SwiftUI-подходом к зависимостям органично.
Настройка
Зависимости регистрируются через extension на Container:
import Factory
extension Container {
var apiClient: Factory<APIClient> {
Factory(self) { DefaultAPIClient() }.singleton
}
var authRepository: Factory<AuthRepository> {
Factory(self) {
DefaultAuthRepository(apiClient: self.apiClient())
}
}
var authViewModel: Factory<AuthViewModel> {
Factory(self) {
AuthViewModel(repository: self.authRepository())
}
}
}
Использование в View:
struct LoginView: View {
@StateObject private var viewModel = Container.shared.authViewModel()
var body: some View {
// ...
}
}
Или через @Injected для сервисов без UI:
class PaymentService {
@Injected(\.apiClient) private var api
}
Scopes и управление жизненным циклом
Factory поддерживает четыре scope:
| Scope | Поведение |
|---|---|
.singleton |
Один экземпляр на всё приложение |
.cached |
Один экземпляр до явного сброса (Container.shared.reset()) |
.shared |
Живёт пока есть хотя бы одна сильная ссылка |
unique (дефолт) |
Новый объект при каждом обращении |
.shared — аналог weak singleton: когда все View, использующие ViewModel, уничтожены, объект освобождается. Полезно для экранов с тяжёлым состоянием, которое не нужно держать вечно.
Тестирование
Главное преимущество Factory — простота переопределения для тестов:
// В setUp() тест-кейса:
Container.shared.authRepository.register {
MockAuthRepository(shouldSucceed: true)
}
// Сброс после теста:
Container.shared.reset()
Не нужен отдельный test container, не нужны моки через протокол-подмену в Production-коде — регистрация просто перезаписывается на время теста.
Типичная проблема: ViewModel пересоздаётся
@StateObject создаёт объект один раз при первом рендере View. Но если View пересоздаётся (например, при переключении Tab), @StateObject сохраняется. Проблема возникает, когда разработчик использует @ObservedObject вместо @StateObject с Factory — тогда ViewModel пересоздаётся при каждом рендере, теряя состояние.
Правило: используем @StateObject для создания ViewModel через Factory на уровне View, @ObservedObject — только для ViewModel, переданной снаружи через init.
Что входит в работу
- Настройка
Containerс регистрацией всех слоёв приложения - Правильные Scope для каждого типа зависимостей
-
@Injectedдля сервисного слоя,@StateObject+ Factory для ViewModel - Переопределение зависимостей для Unit/UI тестов
- Документация по расширению контейнера новыми зависимостями
Сроки
2–3 дня для типичного SwiftUI-проекта. Включает рефакторинг существующего кода под DI-подход, если проект уже написан без контейнера. Стоимость рассчитывается индивидуально.







