# 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 injection
class Car {
let engine: Engine
init(engine: Engine) {
self.engine = engine
}
}
// Property injection
class Car {
var engine: Engine!
}
// Method injection
class 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 system
class 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:

@Observable
class UserViewModel {
private let userRepository: PUserRepository
init(userRepository: PUserRepository = ViewModelFactory.shared.repositoryFactory.userRepository) {
self.userRepository = userRepository
}
}
// Usage in production
let viewModel = UserViewModel() // Uses .shared instance
// Usage in tests
let 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.

My avatar

Thanks for reading my blog post! Feel free to check out my other posts or contact me via the social links in the footer.


More Posts