Basics of Dependency Injection (DI) and Dependency Inversion Principle (DIP) in Swift

Hoyeon Lee
5 min readJun 3, 2023

--

When designing a building, how the lower floors are constructed determines the shape of the upper floors.

Before To Start

When creating code using Swift, we often come across situations like the following:

class A {
let b: B
let c: C
...

init (b: B, c: C) {
self.b = b
self.c = c
...
}
}

class B {

}

class C {

}

...

For example, A can be a ViewModel, while B & C can be considered services or managers performing specific roles.

This code looks easy to write, but it is not a good one from a design perspective. If the code in B & C changes, we inevitably have to modify the code inside of A as well.

This dependent relationship is known as dependency. This time, we will explore several terms related to reducing the coupling between objects and writing more flexible code.

What is Dependency Injection (DI)?

Client, Service, and Injector [Source]

First of all, we need to know about dependency injection. It refers to the way of providing (injection) other objects or data (service) to main object (client) from outside, typically through its initializer.

Now, let’s take a look at the detailed example code.

Example #1:

import UIKit

/* Service #1 */
final class PersonA {
var name: String = "Allen"
var age: Int = 28

func introduce() {
print("My name is \(name) and I'm \(age) years old.")
}
}

/* Service #2 */
final class PersonB {
var name: String = "Eric"
var age: Int = 15

func introduce() {
print("My name is \(name) and I'm \(age) years old.")
}
}

/* Client */
final class PersonInfo {
var person: PersonA // ⭐️ PersonA type

init(person: PersonA) { // ⭐️ PersonA type
self.person = person
}

func printName() {
print(person.name)
}

func printAge() {
print(person.age)
}

func printIntroduction() {
self.person.introduce()
}
}

let personA = PersonA()
let personB = PersonB()

/* Injection */
let personInfo = PersonInfo(person: personA)

personInfo.printName()
personInfo.printAge()
personInfo.printIntroduction()

Example #1 illustrates a situation where the class PersonInfo depends on either PersonA or PersonB through the variable person.

The PersonInfo class can hold an instance of either PersonA or PersonB through its internal variable person. In other words, the dependency occurs in the PersonInfo class.

PersonInfo class currently depends on an instance of PersonA . But If we want to replace it with an instance of PersonB, which part needs to be modified in the code above? My answer would be…

  • var person: PersonA = PersonA()
    var person: PersonB = PersonB()
  • init(person: PersonA)
    init(person: PersonB)
  • let personInfo = PersonInfo(person: personA)
    let personInfo = PersonInfo(person: personB)

Although Example #1 is a very simple and short code, if this were a large-scale project, manually finding and replacing these codes in the PersonInfo class would be an impractical and foolish task.

Implementation of Dependency Inversion Principle (DIP) for More Flexible Codes

Now, we feel that Example #1 is somewhat inefficient. Then, how can we bring about changes to such dependency?

The answer lies in leveraging the dependency inversion principle, which is one of the five basic principles (SOLID) of object-oriented programming design.

It states that abstractions should not depend on details, but details should depend on abstractions. In Swift, abstractions can be represented by protocols. We can utilize protocols to newly implement dependency injection.

Let’s now take a look at the Example #2 that has been modified compared to Example #1.

Example #2:

import UIKit

/* Protocol */
protocol AnyPerson {
var name: String { get }
var age: Int { get }

func introduce()
}

/* Service #1 */
final class PersonA: AnyPerson { // ⭐️ Conform to AnyPerson protocol
var name: String = "Allen"
var age: Int = 28

func introduce() {
print("My name is \(name) and I'm \(age) years old.")
}
}

/* Service #2 */
final class PersonB: AnyPerson { // ⭐️ Conform to AnyPerson protocol
var name: String = "Eric"
var age: Int = 15

func introduce() {
print("My name is \(name) and I'm \(age) years old.")
}
}

/* Client */
final class PersonInfo {
var person: Person // ⭐️ AnyPerson protocol type

init(person: Person) { // ⭐️ AnyPerson protocol type
self.person = person
}

func printName() {
print(person.name)
}

func printAge() {
print(person.age)
}

func printIntroduction() {
self.person.introduce()
}
}

let personA = PersonA()
let personB = PersonB()

/* Injection */
let personInfo = PersonInfo(person: personA)

personInfo.printName()
personInfo.printAge()
personInfo.printIntroduction()

This time, we declare a protocol called AnyPerson, and both the PersonA and PersonB classes conform to this protocol.

The PersonInfo class has a person variable of type AnyPerson protocol. Since both PersonA and PersonB classes adopt the AnyPerson protocol, their instances can be stored in the person variable.

Using this approach, the person variable can hold an instance of either PersonA or PersonB without change of its type. In other words, there is no need to modify many parts of the code inside the PersonInfo class when we want to change the value of person.

In Example #1, the PersonInfo class was tightly coupled to either PersonA or PersonB by its internal code.

However, in Example #2, the PersonInfo class conforms to the AnyPerson protocol, and PersonA or PersonB depends on this AnyPerson protocol, resulting in the inversion of control.

If we want to replace the dependency from an instance of PersonA with an instance of PersonB in the PersonInfo class, what needs to be modified in the code? My answer would be…

  • let personInfo = PersonInfo(person: personA)
    let personInfo = PersonInfo(person: personB)

Now, we only need to modify the code outside the PersonInfo class. By changing just a single variable where the dependency is injected, we can make our codes more flexible and independent.

Summary

Example #1 (left) vs Example #2 (right) [Source]
  • Dependency Injection (DI) is a design pattern and technique that allows objects to receive their dependencies from external sources, rather than creating or managing them internally.
  • Dependency Inversion Principle (DIP) means that both high-level (abstract) and low-level (specific) modules should depend on abstraction which is represented by protocols in Swift.
  • This enables more flexible and reusable code that can be easily extended or modified without affecting the rest of the codebase.
  • By following the DIP, you can write Swift code more modular, scalable, and resilient to changes in requirements.

I’m a beginner learning Swift to become an iOS developer.
Support and comments are always welcome.

--

--