Настройка Dependency Injection з Swinject у iOS
Swinject — один з найбільш зрілих DI-контейнерів для Swift. Використовувався широко до епохи SwiftUI, зараз продовжує застосовуватися у UIKit-проектах та змішаних кодових базах, де SwiftUI існує рядом з UIKit. Якщо проект на чистому SwiftUI — дивіться у сторону Factory або нативного @Environment. Для UIKit-архітектур з MVVM, VIPER або Clean Architecture Swinject залишається актуальним вибором.
Базова настройка контейнера
Точка збору всього графа залежностей — Container. Типова організація: AppAssembly протокол + окремі Assembly-класи по модулям:
import Swinject
class NetworkAssembly: Assembly {
func assemble(container: Container) {
container.register(URLSession.self) { _ in
URLSession(configuration: .default)
}.inObjectScope(.container) // синглтон у рамках контейнера
container.register(APIClient.self) { r in
DefaultAPIClient(session: r.resolve(URLSession.self)!)
}.inObjectScope(.container)
}
}
class AuthAssembly: Assembly {
func assemble(container: Container) {
container.register(AuthRepository.self) { r in
DefaultAuthRepository(
apiClient: r.resolve(APIClient.self)!,
keychain: r.resolve(KeychainService.self)!
)
}
container.register(AuthViewModel.self) { r in
AuthViewModel(repository: r.resolve(AuthRepository.self)!)
}
}
}
Ініціалізація у AppDelegate або SceneDelegate:
let assembler = Assembler([
NetworkAssembly(),
KeychainAssembly(),
AuthAssembly(),
ProfileAssembly()
])
let container = assembler.resolver
Де ломається найчастіше
Force unwrap при resolve. r.resolve(SomeService.self)! — стандартний паттерн у Swinject, але при пропущеній реєстрації це краш у рантаймі. Альтернатива: використовувати Container.loggingBehavior = .verbose у debug-збірках — тоді незареєстровані залежності виводяться у лог до краша. Для критичних залежностей використовуємо перевірку у applicationDidFinishLaunching:
func validateRegistrations(_ container: Container) {
assert(container.resolve(APIClient.self) != nil, "APIClient not registered")
assert(container.resolve(AuthRepository.self) != nil, "AuthRepository not registered")
}
ObjectScope та утечки пам'яті. .container (синглтон) тримає об'єкт на всє час життя контейнера. Для ViewModel у UIKit це проблема: якщо ViewModel зареєстрована у .container та тримає сильну ссилку на ViewController — утечка. ViewModel реєструємо у .transient (новий об'єкт на кожен resolve) або .graph (єдиний об'єкт у рамках одного дерева resolve).
Circular dependencies. Якщо AuthViewModel залежить від Router, а Router залежить від AuthViewModel — Swinject уходит у нескінченну рекурсію при resolve. Вирішується через initCompleted callback для розриву циклу:
container.register(AuthViewModel.self) { _ in AuthViewModel() }
.initCompleted { r, vm in
vm.router = r.resolve(Router.self)
}
Інтеграція з UIKit Navigation
Swinject хорошо працює з Coordinator-паттерном. Координатор отримує resolver та сам розв'язує залежності при створенні екранів:
class AuthCoordinator {
private let resolver: Resolver
init(resolver: Resolver) { self.resolver = resolver }
func showLogin() {
let vm = resolver.resolve(AuthViewModel.self)!
let vc = LoginViewController(viewModel: vm)
navigationController.pushViewController(vc, animated: true)
}
}
Що входить у роботу
- Настройка
ContainerтаAssemblerз розбивкою на модульніAssembly - Реєстрація всіх шарів: мережа, репозиторії, ViewModel, сервіси
- Правильні
ObjectScopeдля кожного типу - Інтеграція з Coordinator або Router
- Валідація реєстрацій у debug-режимі
- Документація графа залежностей
Терміни
2–3 дні для типового проекту з 30–50 реєстрованими типами. Вартість залежить від поточної архітектури та об'єму рефакторингу.







