Настройка 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-підхід, якщо проект вже написан без контейнера. Вартість розраховується індивідуально.







