Setting up Dependency Injection with Swinject in iOS
Swinject — one of most mature DI containers for Swift. Widely used before SwiftUI era, continues applying in UIKit projects and mixed codebases where SwiftUI exists alongside UIKit. Pure SwiftUI project — look at Factory or native @Environment. For UIKit architectures with MVVM, VIPER or Clean Architecture, Swinject remains relevant choice.
Basic Container Setup
Assembly point for entire dependency graph — Container. Typical organization: AppAssembly protocol + separate Assembly classes per module:
import Swinject
class NetworkAssembly: Assembly {
func assemble(container: Container) {
container.register(URLSession.self) { _ in
URLSession(configuration: .default)
}.inObjectScope(.container) // singleton within 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)!)
}
}
}
Initialize in AppDelegate or SceneDelegate:
let assembler = Assembler([
NetworkAssembly(),
KeychainAssembly(),
AuthAssembly(),
ProfileAssembly()
])
let container = assembler.resolver
Where It Most Often Breaks
Force unwrap on resolve. r.resolve(SomeService.self)! — standard Swinject pattern, but on missed registration this crashes at runtime. Alternative: use Container.loggingBehavior = .verbose in debug builds — then unregistered dependencies logged before crash. For critical dependencies use safeResolve with check in applicationDidFinishLaunching:
func validateRegistrations(_ container: Container) {
assert(container.resolve(APIClient.self) != nil, "APIClient not registered")
assert(container.resolve(AuthRepository.self) != nil, "AuthRepository not registered")
}
ObjectScope and memory leaks. .container (singleton) keeps object for entire container lifetime. For ViewModel in UIKit this problem: if ViewModel registered in .container and holds strong reference to ViewController — leak. Register ViewModel in .transient (new object per resolve) or .graph (single object per resolve tree).
Circular dependencies. If AuthViewModel depends on Router, and Router depends on AuthViewModel — Swinject recurses infinitely on resolve. Solved via initCompleted callback to break cycle:
container.register(AuthViewModel.self) { _ in AuthViewModel() }
.initCompleted { r, vm in
vm.router = r.resolve(Router.self)
}
UIKit Navigation Integration
Swinject works well with Coordinator pattern. Coordinator gets resolver and resolves dependencies on screen creation:
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)
}
}
Work Included
-
ContainerandAssemblersetup with modularAssemblysplit - All layers registration: network, repositories, ViewModel, services
- Proper
ObjectScopefor each type - Coordinator or Router integration
- Registration validation in debug mode
- Dependency graph documentation
Timeline
2–3 days for typical project with 30–50 registered types. Cost depends on current architecture and refactoring scope.







