# Simple Dependency Injection in Swift
Table of Contents
Dependency Injection is a great way to decouple your code and make it more testable. In this article I want to show you a simple way to use dependency injection in Swift without external frameworks.
What is dependency injection?Link to heading
Dependency injection is a programming pattern where a component’s dependencies are supplied from the outside rather than created internally. This promotes flexibility and easier testing.
There are three types of dependency injection:
- Constructor injection
- Property injection
- Method injection
// Constructor injectionclass Car { let engine: Engine init(engine: Engine) { self.engine = engine }}
// Property injectionclass Car { var engine: Engine!}
// Method injectionclass Car { func start(engine: Engine) { // ... }}
The problemLink to heading
If we have a complex project with many dependencies, we have to pass them through the whole project. This is not only annoying, but also very error-prone.
In a real project, we have components like ViewModels
, UseCases
, Services
, Managers
, Repositories
, NetworkClients
, DataSources
, and so on.
To keep the code clean and maintainable, we should use dependency injection.
We also have testable code, because we can easily mock the dependencies, assuming we use protocols.
Use external dependenciesLink to heading
While there are many dependency injection frameworks available, this article won’t focus on them. Instead, I’ll demonstrate an uncomplicated way to use dependency injection in Swift.
Keeping in mind the advice from Uncle Bob’s “Clean Architecture” – advocating against building our architecture on external frameworks due to maintenance concerns – how can we implement dependency injection without relying on such frameworks?
Use the Factory patternLink to heading
A simple way is to use the factory pattern for creating our components.
Let’s switch to a real-world context instead of the typical Car/Engine examples.
Let’s say we have a UserRepository
responsible for fetching users from a remote server and a UserViewModel
responsible for displaying users in a list. The UserViewModel
needs the UserRepository
to fetch users. We can use the factory pattern to create the UserRepository
and inject it into the UserViewModel
.
class RepositoryFactory { lazy var userRepository: PUserRepository = { UserRepository() }()}
We use lazy
here because we only want to create the repository when it’s actually needed, not when the factory is initialized. This improves performance and prevents unnecessary object creation.
The PUserRepository represents here the protocol of the UserRepository. We can use this protocol to make the UserRepository testable.
Nested FactoriesLink to heading
In a real world the UserRepository needs a NetworkClient to fetch the users. We can use the factory pattern to create the NetworkClient and pass it to the UserRepository. So we can use a nested factory to create the UserRepository.
// this is the entrypoint of our dependenvy injection systemclass ViewModelFactory { // we use a singleton here, more on that later static let shared = ViewModelFactory() let repositoryFactory = RepositoryFactory()
lazy var userViewModel: PUserViewModel = { UserViewModel(userRepository: repositoryFactory.userRepository) }()}
// ViewModels are using repositories. So we need a factory for the repositories.class RepositoryFactory { let networkClientFactory = NetworkClientFactory()
lazy var userRepository: PUserRepository = { UserRepository(networkClientFactory: networkClientFactory) }()}
// Repositories are using network clients. So we need a factory for the network clients.// Network clients are the end of the dependency injection chain.class NetworkClientFactory { lazy var userNetworkClient: PUserNetworkClient = { UserNetworkClient() }()}
We are using lazy vars here to create the components only when we need them.
The structure of the factories is like a chain and looks like this:
ViewModelFactory -> RepositoryFactory -> NetworkClientFactory
.
With this approach we are able to define the dependencies of the hole project with a simple nested structure.
Use the factoriesLink to heading
Now we can use the factories to create the components. If we take a look at the following SwiftUI example:
struct MainView: View { // create the view model with the factory and // inject it into antoher view var body: some View { UserListView( viewModel: ViewModelFactory.shared.userViewModel ) }}
struct UserListView: View { @ObservedObject var viewModel: PUserViewModel
// constructor injection init(viewModel: PUserViewModel) { self.viewModel = viewModel }
var body: some View { List(viewModel.users) { user in Text(user.name) } }}
Why Singleton?Link to heading
We can use the singleton pattern to provide global access to the factories. It was very annoying to pass the factories throughout the entire project or create a new instance of the factories every time, especially since I only need them once. By doing so, we can avoid passing the factories throughout the entire project.
You can also use constructor injection with a default parameter to make testing easier:
@Observableclass UserViewModel { private let userRepository: PUserRepository
init(userRepository: PUserRepository = ViewModelFactory.shared.repositoryFactory.userRepository) { self.userRepository = userRepository }}
// Usage in productionlet viewModel = UserViewModel() // Uses .shared instance
// Usage in testslet mockRepository = MockUserRepository()let viewModel = UserViewModel(userRepository: mockRepository) // Uses injected mock
ConclusionLink to heading
In this article I showed you a simple way to use dependency injection in Swift. We used the factory pattern to create our components through a dependency chain and pass them through the project into a view. We used a singleton to store the factories and use them throughout our project. There are no configuration files or external dependencies. We just used Swift to create our dependency injection system.
The only disadvantage of this approach is that we have to create the factories manually. But most of this work will be done at the beginning of the project.
This approach follows Uncle Bob’s advice from “Clean Architecture” - never build your architecture on external frameworks. Imagine coming back to your project after 2 years and finding outdated dependency injection libraries with breaking changes, deprecated APIs, or maintenance issues. You’ll be frustrated trying to update everything just to get your project running again.
With this simple, self-built solution, you have full control. It’s pure Swift code that will work years from now without any external maintenance headaches.