Настройка 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-сборках — тогда незарегистрированные зависимости выводятся в лог до краша. Для критичных зависимостей используем safeResolve с проверкой в 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 регистрируемыми типами. Стоимость зависит от текущей архитектуры и объёма рефакторинга.







