At some point while developing for iOS, this question comes up.
“If I can just pass it through the initializer, do I really need a DI container?”
As a side project grows, everyone eventually faces this dilemma. I established my criteria by using both Swinject and Factory in real projects.
Let me give you the conclusion first.
If you have fewer than 20 screens, constructor injection alone is sufficient without a DI container.
When the dependency graph becomes complex and mock replacement becomes frequent, that is the time to adopt one.
And if you adopt one, choose Factory.
I’ll explain when to adopt it and why Factory, in the order I experienced it.
Before Adopting a DI Container, Check This First
Let’s start by clearing up the part that is easy to confuse.
Dependency injection (DI) and a DI container are different.
Dependency injection is the design practice of supplying required objects from outside. It requires no library at all.
// This is already excellent dependency injection
final class LoginViewModel {
private let authService: AuthService
init(authService: AuthService) {
self.authService = authService
}
}
A DI container, on the other hand, is a tool that manages where and how these objects are created in one place.
If constructor injection is enough, it is still too early for a container.
I use these signals to decide when to adopt one.
- When initialization code is repeated across screens and passing objects starts to feel burdensome
- When you frequently replace objects with fakes in tests
- When multiple parts of the app need to share the same instance globally, with singleton-like semantics
- When the team grows and people often ask, “Where is this object created?”
If two or more of these apply, that is when I start considering a container.
Swinject vs. Factory: What’s the Difference?
Swinject is a traditional runtime container in the Swift ecosystem that has been used since 2015. You register objects in Container and retrieve them with resolve. The problem is that resolve returns an optional. If you forget a registration, compilation succeeds normally, and the app crashes only when you force-unwrap nil on that screen.
Factory was created to address precisely these shortcomings of traditional containers. Michael Long, who created Resolver, redesigned it based on that experience. The core idea is that “definition is registration.” Since factories are defined as container properties, forgetting to register one is not even a possibility. An invalid reference produces an error at compile time, not runtime.
Here is a table of the differences I observed as of 2026.
| Item | Swinject | Factory |
|---|---|---|
| Registration and resolution | Runtime resolve (returns optional) | Type-based definitions + property wrappers |
| When registration is missing | Runtime nil/crash | Prevented in advance by a compile error |
| Scope management | Supported | Supported (.singleton, .cached, .shared, etc.) |
| External dependencies | Separate framework dependency | Lightweight single package |
| Learning curve | Somewhat steep | Relatively gentle |
People used to say, “Choose Swinject when you need complex scope management,” but that is no longer true. Factory also supports singleton, cached, and shared scopes. Factory does what Swinject did, but more safely.
So the Conclusion Is Factory
These were the criteria I actually used.
If you are adopting one from scratch, Factory is all you need
Whether it is a solo or team project, Factory was easy to adopt because you can start with a single package and no external dependencies. Most importantly, it structurally eliminates the class of failures where forgetting a registration causes a runtime crash.
// Factory: Definition is registration
extension Container {
var authService: Factory<AuthService> {
self { LiveAuthService() }.singleton // Scopes in one line
}
}
// The consumer
@Injected(\.authService) private var authService
Replacing it with a mock in tests also takes one line.
Container.shared.authService.register { MockAuthService() }
When Should You Keep Swinject?
When maintaining a large codebase already built with Swinject. There is no reason to tear out an assembly structure that works well right away. Even in such projects, however, more teams are gradually moving new modules to Factory. It is now difficult to find a reason to choose Swinject for a new adoption.
Frequently Asked Questions (Q&A)
Q. Can’t I start with a container from the beginning?
I won’t stop you, but I don’t recommend it. When the dependency graph is simple, a container adds another layer of indirection and actually makes the code harder to trace.
Q. Can I migrate from Swinject to Factory?
Yes. A gradual migration by temporarily running both containers and moving one module at a time is realistic. You will need to revisit registration points and scope settings, so it can take time for a large codebase, but once migrated, you no longer have to worry about runtime crashes.
Q. Which is better for a SwiftUI project?
Definitely Factory. Since it is based on property wrappers, it fits naturally with SwiftUI’s declarative style, and injecting mocks in previews is simple.
The cleanest approach is to adopt a DI container when you actually need one.
If you decide to adopt one, go with Factory without overthinking it. It was created to address the shortcomings of existing containers, so it works well whether you start small or scale it up. First, count how many of the four adoption signals above apply to you.

