Skip to Content
Course content

248: Dependency Injection Patterns in Swift

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent way too many hours of my career fighting with "untestable" code. Usually, the culprit is the same: a class that decides exactly which other classes it wants to work with, hardcoding those dependencies right into its own initializer. It feels efficient at first, but the moment you try to write a unit test without triggering a real network call or writing to a production database, you'll hit a wall.

Let's look at how to fix this using Dependency Injection (DI) patterns in Swift. We're going to build a simple WeatherViewModel that fetches the current temperature.

The mistake of hardcoding dependencies

If I were rushing a feature, I might write something like this. I'll just create the service inside the view model and call it a day:

class WeatherService {
    func fetchTemperature() -> String {
        // Imagine a real URLSession call here
        return "72°F"
    }
}

class WeatherViewModel {
    private let service = WeatherService()
    
    func getWeather() -> String {
        return service.fetchTemperature()
    }
}

Now, here is where I've messed up. If I want to test WeatherViewModel, I'm forced to use the real WeatherService. If the API is down or the internet is flaky, my tests fail. The WeatherViewModel is "tightly coupled" to the concrete WeatherService class. It's stuck.

Breaking the tie with a Protocol

To fix this, we need to stop the view model from caring which class is doing the work, as long as that class knows how to fetch a temperature. In Swift, the best way to do this is with a protocol. I'm going to define the "behavior" separately from the "implementation."

protocol WeatherProviding {
    func fetchTemperature() -> String
}

class RealWeatherService: WeatherProviding {
    func fetchTemperature() -> String {
        return "72°F" // Real API logic goes here
    }
}

By doing this, I've created a contract. Anything that conforms to WeatherProviding can now be used by our view model.

Injecting the dependency via the initializer

Now for the actual "Injection" part. Instead of the view model creating its own service, I'm going to tell the view model: "You don't make the service; I'll give you one when I create you." This is called Constructor Injection, and it's the most robust DI pattern in Swift.

class WeatherViewModel {
    private let service: WeatherProviding
    
    // We inject the dependency here
    init(service: WeatherProviding) {
        self.service = service
    }
    
    func getWeather() -> String {
        return service.fetchTemperature()
    }
}

Notice that WeatherViewModel no longer knows RealWeatherService exists. It only knows about the protocol. This is a huge win for flexibility.

Testing with a Mock service

The real magic happens when we write our tests. I can now create a "Mock" version of the service that returns a hardcoded value instantly, without ever touching the network. This makes tests fast and deterministic.

class MockWeatherService: WeatherProviding {
    var mockTemperature = "0°F"
    
    func fetchTemperature() -> String {
        return mockTemperature
    }
}

// In our test file:
let mock = MockWeatherService()
mock.mockTemperature = "32°F" 

let viewModel = WeatherViewModel(service: mock)
print(viewModel.getWeather()) // Always returns "32°F" regardless of the weather outside

I've now completely decoupled the business logic of the view model from the data-fetching logic of the service. If we ever decide to switch from a REST API to GraphQL or a local database, we just create a new class conforming to WeatherProviding and swap it out at the top level of the app. The view model doesn't change a single line of code.




📋 Practical Task

Exercise: Decoupling a UserProfileViewModel

You have a UserProfileViewModel that is currently tightly coupled to a DatabaseManager. Your task is to refactor this code to use Constructor Injection so that it can be tested with a mock database.

Current Code:

class DatabaseManager {
    func getUserName(id: Int) -> String {
        return "Real User from DB"
    }
}

class UserProfileViewModel {
    private let db = DatabaseManager()
    
    func displayName(for id: Int) -> String {
        return db.getUserName(id: id)
    }
}

Your Requirements:

  • Create a protocol named UserProviding that defines the getUserName(id:) method.
  • Make DatabaseManager conform to UserProviding.
  • Modify UserProfileViewModel to accept any UserProviding object through its initializer.
  • Create a MockUserDatabase class that conforms to UserProviding and returns the string "Mock User".
  • Instantiate the UserProfileViewModel using the MockUserDatabase.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.