Dependency injection patterns in Swift
One of the most useful patterns in software development — and one that is available in many languages, not just Swift — is dependency injection. When I first learned about it over a decade ago, I started using it everywhere possible. It’s a simple idea with a surprisingly big impact. Here’s why it’s worth mastering.

Why is it so useful?
One of the biggest advantages of using dependency injection is that it makes our code immediately more testable. Even in projects full of singletons, we can break apart large chunks of tightly coupled code into smaller, more loosely coupled components. Our implementations rely more on protocols (interfaces) rather than concrete classes, making it easier to swap or replace them.
By depending on abstractions rather than concrete implementations, each component can focus solely on its own responsibilities, without knowing the details of how other parts of the system are managed. This makes it easier to reuse components across different parts of an app. We can easily replace services with mocks during testing or switch implementations when requirements change.
A basic example of how to use initializer-based dependency injection
Imagine that we use in our ViewModel, a couple of services which are singletons. Instead of referencing them directly in the method definition like this:
func makeUserHappy() {
let currentUser = UserService.shared.currentUser
PremiumService.shared.grantSuperPowers(to: currentUser)
AdService.shared.removeAds()
}
We can move references outside of the method:
class ExampleViewModel {
private let userService = UserService.shared
private let premiumService = PremiumService.shared
private let adService = AdService.shared
func makeUserHappy() {
let currentUser = userService.currentUser
premiumService.grantSuperPowers(to: currentUser)
adService.removeAds()
}
}
The next step of moving it away would be to pass these references to the initializer:
class ExampleViewModel {
private let userService: UserService
private let premiumService: PremiumService
private let adService: AdService
init(userService: UserService = .shared,
premiumService: PremiumService = .shared,
adService: AdService = .shared) {
self.userService = userService
self.premiumService = premiumService
self.adService = adService
}
func makeUserHappy() {
let currentUser = userService.currentUser
premiumService.grantSuperPowers(to: currentUser)
adService.removeAds()
}
}
It’s already looks much better. Now we can create mock variants of these classes for our tests using inheritance like this:
class MockUser: UserService {
override var currentUser: String {
User(id: -1, name: "Bob")
}
}
class MockPremiumService: PremiumService {
private var superUsers: [Users]
override func grantSuperPowers(to: User) {
user.powers.append(.premiumPowers)
superUsers.append(user)
}
}
Inheritance can help us replace the implementation of exposed service methods, but the rest of the underlying logic remains the same. This can lead to serious side effects, especially if the internal logic of our service classes is complex. A much better approach is to rely on protocol conformance instead of concrete classes in our ViewModel.
Taking this into consideration, we can end up with something like this:
class ExampleViewModel {
private let userService: UserServiceProtocol
private let premiumService: PremiumServiceProtocol
private let adService: AdServiceProtocol
init(userService: UserService = UserService.shared,
premiumService: PremiumService = PremiumService.shared,
adService: AdService = AdService.shared) {
self.userService = userService
self.premiumService = premiumService
self.adService = adService
}
func makeUserHappy() {
let currentUser = userService.currentUser
premiumService.grantSuperPowers(to: currentUser)
adService.removeAds()
}
}
Then we can implement MockUserService and the real UserService independently, without needing to mix parts of the real service into the mock — which is exactly what tends to happen when relying on inheritance.
// Protocols
protocol UserServiceProtocol {
var currentUser: User { get }
func fetchUserData() async
}
protocol PremiumServiceProtocol {
func grantSuperPowers(to user: User)
func purchase(_ product: Product)
}
// Real implementations
class UserService: UserServiceProtocol {
var currentUser: User {
persistence.currentUser
}
func fetchUserData() async {
if let user: User = try? await network.request("/user/me") {
persistence.saveUser(user)
}
}
}
class PremiumService: PremiumServiceProtocol {
func grantSuperPowers(to user: User) {
var updated = user
updated.isPremium = true
database.saveUser(updated)
}
func purchase(_ product: Product) async throws {
try await storeKit.startPurchase(productId: product.id)
}
}
// Mock implementations
class MockUserService: UserServiceProtocol {
var currentUser: User = User(id: "test-id", name: "TestUser", isPremium: false)
var fetchUserDataCalled = false
func fetchUserData() async {
fetchUserDataCalled = true
// Instant predictable mock result
currentUser = User(id: "mock-123", name: "MockUser", isPremium: true)
}
}
class MockPremiumService: PremiumServiceProtocol {
var grantedUsers: [User] = []
var purchasedProducts: [Product] = []
func grantSuperPowers(to user: User) {
grantedUsers.append(user)
}
func purchase(_ product: Product) {
purchasedProducts.append(product)
}
}
What we have here is called initializer-based dependency injection because we pass our dependencies through the initializer. In my opinion, it’s the best basic way to implement dependency injection for iOS applications. It’s a simple concept that anyone can learn quickly. It offers excellent testability: all dependencies are injected at object creation, so tests can consistently provide mocks or stubs. There’s no risk of uninitialized dependencies, which makes tests more reliable.
It’s also characterized by strong modularity. Components are fully decoupled from concrete implementations, and each dependency is clearly defined in the initializer, making it easy to swap implementations.
However, there are several other ways to handle dependency injection, which I will briefly explain in the next sections.
Property injection
class ExampleViewModel {
// Dependencies are optional until injected
var userService: UserServiceProtocol!
var premiumService: PremiumServiceProtocol!
var adService: AdServiceProtocol!
func makeUserHappy() {
let currentUser = userService.currentUser
premiumService.grantSuperPowers(to: currentUser)
adService.removeAds()
}
}
// Somewhere else
let vm = ExampleViewModel()
vm.userService = UserService.shared
vm.premiumService = PremiumService.shared
vm.adService = AdService.shared
This kind of dependency injection is usually seen in views containing IBOutlets, or in cases where we depend on view components that need to be initialized first. It’s more flexible because we don’t have to pass dependencies through the initializer—the injection timing is more flexible. However, this comes at a cost: there’s a risk of calling dependencies before they are injected, which can crash the app. It’s also harder to enforce correctness. That’s why I prefer the initializer-based dependency injection described in the previous section.
It can be handy for testing, as we can easily swap dependencies after object creation, but it’s also easier to break and make our tests flaky.
It’s not optimal in terms of modularity because objects rely on external code to inject dependencies before use. The contract of required dependencies is less explicit, making it easier to misuse the component.
In my opinion, code clarity can also suffer because readers of our source code must track when and where dependencies are injected.
Method injection
class ExampleViewModel {
func makeUserHappy(
userService: UserServiceProtocol,
premiumService: PremiumServiceProtocol,
adService: AdServiceProtocol
) {
let currentUser = userService.currentUser
premiumService.grantSuperPowers(to: currentUser)
adService.removeAds()
}
}
// Usage:
let vm = ExampleViewModel()
vm.makeUserHappy(
userService: UserService.shared,
premiumService: PremiumService.shared,
adService: AdService.shared
)
This kind of dependency injection is more functional. We don’t need to store our dependencies as properties, and it’s also easy to test methods implemented this way. On the other hand, calling these methods can become quite verbose. It’s also not very optimal if we want to call the method from many different places. In that case, the problem of keeping everything consistent is simply delegated elsewhere, and we still need to manage it somewhere.
It’s very testable. Each method call receives its dependencies explicitly, making it easy to test in isolation. However, it may take some extra effort to setup dependencies in our tests.
In terms of modularity, we also can find a strong advantage of this approach. Objects don’t need to store dependencies as properties, reducing hidden state. But on the other hand, it can become cumbersome if multiple methods need the same dependencies repeatedly.
Summary
There are three basic approaches to dependency injection in Swift. What do you think about these approaches? Do you use dependency injection in your projects? Or maybe you use a dependency injection framework—if so, what do you think about these lightweight approaches?
Regardless of the approach you choose, the key is to make your code more modular, testable, and maintainable. Even lightweight dependency injection can significantly improve the architecture of your Swift projects without adding unnecessary complexity. Experiment with different techniques and see which one fits your team and project style best.
Michał Cichoń is a software engineer based in Kraków, Poland.
With over 15 years of experience in web and mobile development, he specializes in building iPhone and iPad applications using Swift and Objective-C.
He currently works on a social media app developed by a small, data-driven team, where experiments and A/B testing shape user experience. Over the years, he has collaborated with financial institutions, biomedical companies, and startups from New York, Berlin, and beyond.
