Back to blog

Dependency Injection in Swift


Aasif Khan
By Aasif Khan | Last Updated on August 29th, 2023 4:05 pm | 5-min read

Dependency Injection sounds complex, but it’s actually a surprisingly simple concept. In this app development tutorial, you’ll learn how dependency injection works. Understanding dependency injection (DI) will greatly improve your code quality and productivity as a Swift developer.

Many software design principles, like Don’t Repeat Yourself and SOLID, have one thing in common: they make your code more maintainable by making it modular. Instead of creating one big pile of code, you create separate your code into modules that interact with each other. Dependency injection also helps increase the modularity of your code.

Heard of the term “spaghetti code” before? It’s a codebase where every component is connected with every other component. It’s chaotic, hard to maintain, and even harder to debug. You want to avoid spaghetti code at all costs, and dependency injection helps with that.

Here’s what we’ll discuss:

  • What dependency injection (DI) is and how it works
  • How you can use DI with Swift on iOS
  • 3 approaches to provide dependencies to objects
  • How dependency injection affects the modularity of your code
  • Advantages of DI, such as mocking and stubbing

Working with Dependencies

We’re going to start with finding out what a dependency really is, and how that factors into dependency injection.

You’ll soon realize that the term “dependency injection” is, in fact, a perfect word for this development concept. You’re literally injecting a dependency with code.

What’s a dependency? A dependency is an object that other code depends upon to function. Let’s look at this code example.

protocol Propulsion {
func move()
}

class Vehicle
{
var engine: Propulsion

init() {
engine = RaceCarEngine()
}

func forward() {
engine.move()
}
}

Imagine the following scenario:

  • You have a special car: you can switch out its engine for anything you want: a jet engine, a rocket engine, a race car engine…
  • To make sure any engine actually works, you’ve defined a rule for engines for your car: they must be able to move

You see two things in the code:

  • A protocol called Propulsion
  • A class called Vehicle

The protocol Propulsion defines one function: move(). When a class wants to conform to the protocol, it will need to implement that move() function.

The class has one property engine, of protocol type Propulsion. You can assign any type of object to engine as long as it conforms to the Propulsion protocol. This is called protocols as a type.

When an instance of Vehicle is initialized, with the init() function, an instance of RaceCarEngine is assigned to the property engine. Then, when the function forward() is called, the vehicle will call the move() function on the engine object.

The protocol Propulsion defines the rules for the engine, and within the Vehicle functions the engine is initialized and used to call the move() function.

Here’s the actual car engine:

class RaceCarEngine: Propulsion {
func move() {
print(“Vrrrooooommm!!”)
}
}

The RaceCarEngine class implements the Propulsion protocol, and defines an implementation for the required move() function.

Here’s how you use them together:

var car = Vehicle()
car.forward() // Output: Vrrrooooommm!!

You define a car variable of type Vehicle, and call the forward() function to move the car with the engine.

What is the dependency here?

The dependency is the RaceCarEngine class, inside the init() function of Vehicle. The Vehicle class is tightly coupled with the RaceCarEngine class, because the Vehicle class directly references the RaceCarEngine class in its initializer.

You’ve directly referenced RaceCarEngine from Vehicle, so the Vehicle class now depends on the RaceCarEngine to function. This is a dependency. You want to avoid it, if you can, because right now it’s hard to use the Vehicle class without the RaceCarEngine class.

How can we get around that? That’s where dependency injection comes in.

Dependency Injection in Swift

There’s no real way to avoid dependencies altogether, and that’s a good thing. Dependencies aren’t bad! Your app’s code works together; it’s interconnected. Writing code on top of other code – abstraction – is what makes code work.

It’s easy to create tightly coupled code with bad dependency management. That’s the problem we’re trying to solve here. A typical sign of spaghetti code is non-modular, tightly coupled code and no structural approach to managing dependencies.

Let’s go back to the Vehicle class from the previous section. How can we use dependency injection to improve the code? Check this out:

class Vehicle
{
var engine: Propulsion

init(engine: Propulsion) {
self.engine = engine
}

func forward() {
engine.move()
}
}

It’s a subtle change, but it makes all the difference. First, notice that the Vehicle class does not mention the RaceCarEngine at all!

Instead of hard-coding the RaceCarEngine object into the init() function of Vehicle, we’ve now added a parameter to the initializer. The parameter is called engine, its type is Propulsion (the protocol), and it’s used to set the engine property of Vehicle upon initialization.

Here’s how everything works together:

let fastEngine = RaceCarEngine()

var car = Vehicle(engine: fastEngine)
car.forward() // Output: Vrrrooooommm!!

In the above code, we’re first creating an instance of RaceCarEngine. Then, an instance of Vehicle is initialized.

See the engine parameter? The fastEngine object is injected into the Vehicle object from the outside. This is dependency injection! Both classes still depend on each other, but they’re not tightly coupled anymore. They’re modular; you can use one without the other.

Let’s look at some more code:

class RocketEngine: Propulsion {
func move() {
print(“3-2-1… IGNITION!… PPPPSSSSCHHHHOOOOOOOOOMMMMMM!!!”)
}
}

Yeah, it’s a rocket engine alright! Just like RaceCarEngine, the RocketEngine class conforms to the Propulsion protocol.

You can switch out the race car engine in your car for a rocket engine, with zero effort. Check this out:

let rocket = RocketEngine()

var car = Vehicle(engine: rocket)
car.forward() // 3-2-1… IGNITION!… PPPPSSSSCHHHHOOOOOOOOOMMMMMM!!!

In the above code, we’ve created an instance of RocketEngine. When a Vehicle object is initialized, we’re injecting the rocket object by using the initializer’s engine parameter.

Just like before, we’re creating a dependency and inject that into the component that relies upon it. Instead of tightly coupling the components, hard-coding them into the class, we introduce modularity with dependency injection.

A key component to make dependency injection work, of course, is the protocol as a type. Both engines conform to that protocol, which is why we can switch ’em. Do you always need a protocol for this? No. You can use one of Swift’s many other powerful features, such as subclassing, generics and opaque types.

Don’t get this the wrong way, though! You could have switched out the engine without using dependency injection, in our initial example. You simply could have changed the Vehicle class, right?

3 reasons why that might not work:

  1. You don’t have access to the Vehicle class code, because someone else wrote it – or it’s a framework whose code you cannot change
  2. The rocket engine class isn’t ready yet, so you’re forced to create a similar “fake” class for testing purposes, and you don’t want to go back and change everything when the class is ready (mocking)
  3. You want to test the rocket engine class, so you need to be able to dynamically switch it for a diagnostics rocket engine that logs everything it does (unit testing)

Let’s discuss these 3 use cases in the next section…

Do you really need the protocol, to make DI work? It’s a point of discussion. Depending on your code, you could allow the injection of a concrete object that’s configurable. You could even substitute a dependency with a subclass!

When to Use Dependency Injection

Dependency injection is useful in the following scenarios:

  1. You want to change the implementation of code you don’t have access to
  2. You want to “mock” or fake behavior in your code during development
  3. You want to unit test your code

Let’s get to it.

Code You Can’t Access (Yet)

You’re working with code you don’t have access to all the time! Think about iOS frameworks or a 3rd-party library. Even though, strictly speaking, you have “access” to the code – you can’t change it without breaking it!

The Cocoa Touch SDK has a neat solution for that: delegation. Delegation is a programming design pattern that enables a class to hand-off some of its responsibilities to another class instance.

In many Cocoa Touch classes you can assign your own object to protocolized delegate properties. The framework classes then call functions on your delegate objects. Since you have control over the delegate objects, you can change the behavior of framework classes without changing their code directly.

Dependency injection is a similar technique, in the sense that it allows you to customize the implementation of code that would otherwise be unreachable.

Consider the following scenario:

  • You’re working with another iOS developer. The other developer is responsible for building a WebAPI class, that you depend on.
  • You agree that the WebAPI must implement a function getItems(), so you come up with a protocol called API that includes this function
  • Until the other developer is done creating the WebAPI, you create your own stub API called FakeAPI. Its API returns a simple list of items.
  • You use dependency injection in your code to be able to work with any object that conforms to API. You don’t care how it works, as long as it has that getItems() function!
  • When the real WebAPI is ready, you switch the classes and inject the right object into your code. You only have to change 1-2 lines, tops.

When the time comes to replace the API with a brand new component, somewhere in the future, you only have to code the new API implementation. If it conforms to the protocol, and exposes the same getItems() function, it should work perfectly fine. Yay, modularity!

Modularity and composability, i.e. separate things that you can combine, lie at the heart of the Unix philosophy. You find the same design principles in Swift and iOS.

Mocking

With dependency injection you can easily switch out dependencies. In the previous example, the one with the car engine, you saw that you could change the engine implementation of the car.

Say that you were really building a rocket engine. You’ve got two types of engines:

  • The actual rocket engine
  • A diagnostics rocket engine

The diagnostics rocket engine sends logging data to an external service. When it starts up, it’ll send a I’m starting up message to the logging service. The rocket engine also does more calculations to generate diagnostics data, so it’s more resource intensive.

With dependency injection you can switch out the actual rocket engine, and the diagnostics engine without changing any core rocket code. This is similar to switching one component for a stub component, like we’ve discussed before.

Stubbing and mocking is similar, but different. A stub is a function (or a class) that has the correct signature, but it’s (almost) empty – it doesn’t do anything. A mock is a function (or class) that has a fake implementation, that’s typically much simpler than its real-world counterpart.

  • You use stubbing to quickly put a replaceable dependency in place. It doesn’t do anything, so the idea is to replace it later with the real thing.
  • You use mocking to replace a dependency with a component that’s fake, or has a different implementation than the real thing.

A common scenario for mocking is generating diagnostics data. For example, you’ve got a Log class that normally gathers logging data in a text file when you’re developing your app. In a production environment, you don’t want to log to a text file – or log anything at all – so you replace the Log component with a mock substitute.

Unit Testing

Unit testing is a software development practice that tests the output of code, based on a given input, and compares it against predefined value. When the output matches the predefined value, the test is successful. You know that the implementation, the unit you’re testing, matches expectations.

Compare it to a simple pocket calculator. You test whether the addition function works correctly by calculating 1 + 1. When the result is 2, you know it works, and when the result is something else, you know the function is broken.

This is useful if you’re going to change the implementation of some code. You first design the test and run it against your current code, to ensure that the test is correct. You then change the code that’s tested, called Unit Under Test, and run the test again.

If the test succeeds, you know that the new code’s output is OK. Other code that depends on the output of the tested code should be unaffected by the new changes, and continue to run OK.

Unit testing is typically labor intensive, so you want to automate as much as possible. It’s easier to test modular code that isn’t tightly coupled. With dependency injection you can quickly inject a testing class as a dependency without having to replace entire blocks of code.

You can test the dependency itself, i.e. if the rocket engine runs smoothly, or the code that uses the dependency, i.e. if the rocket moves based on input from the rocket engine.

With unit testing you typically replace components in the code with mocks or stubs. Imagine you want to test a WebAPI. Instead of downloading items from the internet, the networking requests code is replaced with a simpler alternatives that reads the same data from a local file. You can then test if that data ends up OK in a view controller, for example.

Approaches for Dependency Injection

You can inject dependencies in Swift with 2 approaches:

  1. Initializer injection: provide dependency via the initializer init()
  2. Property injection: provide dependency via a property (or setter)

You can use either, but in a few use cases, one is better than the other.

  • It’s recommended to use initializer injection for a dependency that doesn’t change during the lifetime of the dependant object
  • When the dependency can change during the lifetime of the dependant object, or when it’s nil at initialization, it’s better to use property injection

Initializer Injection

Before, we’ve used initializer injection to provide the RocketEngine object to an instance of the Vehicle class. Like this:

let car = Vehicle(engine: rocketEngine)
In the above code we’re using the initializer function Vehicle(engine:) to provide a dependency to the Vehicle instance. That’s why it’s called initializer injection, of course!

This approach is sometimes called constructor injection, because in other programming languages initializers are sometimes called constructors. Good to know!

Property Injection

Property injection isn’t much different. Instead of injecting the dependency as an initializer argument, you directly assign a value to the property:

var car = Vehicle()
car.engine = rocketEngine
In the above code, we’re passing the rocketEngine object to the Vehicle instance by using a stored property.

You’re not required to use one or the other, but you can see how initializer injection suggests that a dependency is used throughout the lifetime of the dependant object. It’s injected right there at the inception of the object.

Method Injection

You can also use a setter function to inject a dependency. This is called method injection. Strictly speaking, functions that belong to a class are called methods.

car.setEngine(rocketEngine)
Method injection is much less common in Swift than property or initializer injection. Setter functions, like the one above, aren’t commonly used. Instead, you typically use a property to assign an object directly.

It’s good to know method injection is an alternative approach, though. You could use it to provide a dependency for the lifetime of a function, for example.

Further Reading

Understanding dependency injection is a great step towards creating more modular, more maintainable and less error-prone code. In this tutorial, we’ve discussed how you can use dependency injection in your project.

Here’s what we focused on:

  • How you can use dependency injection (DI) with Swift on iOS
  • What DI is and how it works
  • Advantages of DI, such as mocking and stubbing
  • 3 approaches to provide dependencies to objects
  • How dependency injection affects the modularity of your code


Aasif Khan

Head of SEO at Appy Pie

App Builder

Most Popular Posts