Dependency injection in iOS and Swift using Swinject and SwinjectStoryboard

Anurag Ajwani
Onfido Product and Tech
11 min readMar 11, 2020

--

Most apps that we build for iOS rely on some functionality developed and serviced by another provider. For example allowing you to login with Google.

Many apps also rely on data and functionality which is available outside the iOS operating system and on the internet. For example, in the case of a food delivery business we would require a list of restaurants open. Once the user selects the desired restaurant then the menu for such restaurant is required.

You could create a new app. When the restaurants screen is loaded, you call the server endpoint to fetch the list of restaurants. Once the server responds, you display the information to the user. The user then selects the desired restaurant and then the menu screen is displayed. When the menu screen is loaded then you fetch the menu for the selected restaurant and display the menu to the user.

Food delivery app flow

The app depends on a server to function. The restaurants screen depends on the code that fetches the list of restaurants. The menu screen depends on the code that fetches the menu for the selected restaurant. The dependencies required are dictated by the flow of the app.

What if we’d like to have multiple providers for our information and would like to switch where we fetch our data from? Or what if we’d like to have a stub provider for the development or testing of this app?

What we need is a simple way of changing where the app fetches the information from at runtime. For such in this post we will explore dependency injection as a solution. Dependency injection will allow us to specify the dependency required for this app at runtime instead of compile time.

In this tutorial we’ll be using Xcode 11.3 and Swift 5. Basic knowledge of Swift and iOS is required for this tutorial.

Setting the context

Let’s further explore the food delivery app example. The app’s restaurant list screen could look like the following without dependency injection:

class RestaurantListViewController: UIViewController {    private let restaurantLister = RestaurantLister()    override func viewDidLoad() {
super.viewDidLoad()
self.restaurantLister.get(onCompletion: { restaurants in
self.displayRestaurants(restaurants)
})
}
...

And the RestaurantLister class could look like the following:

class RestaurantLister {    func get(onCompletion: @escaping ([Restaurant]) -> ()) {
let url = URL(string: "https://api.mybusiness.com/restaurants")
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
print(String(data: data, encoding: .utf8)!)
}
task.resume()
}
...

Note in the example above the compiler knows the concrete class RestaurantListViewController uses for fetching the restaurant list — RestaurantLister. Also note that the dependency is controlled by the flow of the app. That is when the user lands on the restaurant list screen, the view controller instantiates and stores a reference to the concrete class that will be used to fetch the list of restaurants.

What is dependency injection and why use it?

Dependency injection is a form of moving the control of specifying dependencies away from classes. Away where? The idea is to move it to a piece of code that assembles the class to be injected.

In our previous example RestaurantListViewController was assembling itself by creating the instance of the dependency it required: RestaurantLister. Instead of creating the instance of the RestaurantLister class, the view controller could just specify RestaurantLister as a requirement. This could be either as a property, method or as part of the constructor of the class.

Property:

class RestaurantListViewController: UIViewController {    var restaurantLister: RestaurantLister!

Method:

class RestaurantListViewController: UIViewController {    private var restaurantLister: RestaurantLister!    func setRestaurantLister(_ lister: RestaurantLister) {
self.restaurantLister = lister
}

Constructor:

class RestaurantListViewController: UIViewController {    let restaurantLister: RestaurantLister    init(restaurantLister: RestaurantLister) {
self.restaurantLister = restaurantLister
...
}

In the code above RestaurantListViewController is no longer responsible in creating the instance of RestaurantLister. Rather some other piece of code should assemble RestaurantListViewController instances. But who is responsible of assembling RestaurantListViewController ? Something as simple as a factory function:

func getRestaurantLister() -> RestaurantLister {
return RestaurantLister()
}
func getRestaurantListViewController() -> RestaurantListViewController {
let lister = getResaurantLister()
return RestaurantListViewController(restaurantLister: lister)
}

But what have we really gained here? The RestaurantListViewController still uses the same RestaurantLister. In the code above only the instantiation of the concrete class RestaurantLister has moved outside. Furthermore a single line of code has moved to multiple lines. Additionally you are now required to call multiple functions. So nothing has been gained whilst the code is more complex. To make dependency injection work for us we have to lower the cost of change.

We can lower the cost of change using dependency injection by using protocols. First let’s make RestaurantLister a protocol. RestaurantListViewController doesn’t care how RestaurantLister works. It only cares that it gets from it a list of Restaurant. We first have to define the RestaurantLister contract using a protocol:

protcol RestaurantLister {
func get(onCompletion: @escaping ([Restaurant]) -> ())
}

Then we can create multiple implementations of said protocol. The default:

class ApiRestaurantLister: RestaurantLister {    func get(onCompletion: @escaping ([Restaurant]) -> ()) {
let url = URL(string: "https://api.mybusiness.com/restaurants")
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
print(String(data: data, encoding: .utf8)!)
}
task.resume()
}
}

And the stub:

class StubRestaurantLister: RestaurantLister {    func get(onCompletion: @escaping ([Restaurant]) -> ()) {
let restaurants = [Restaurant(name:"Bob's Pizza")]
onCompletion(restaurants)
}
}

And then change the getRestaurantLister function to:

func getRestaurantLister() -> RestaurantLister {
let isDevelopmentOrTest = getIsDevelopmentOrTest()
if isDevelopmentOrTest {
return StubRestaurantLister()
} else {
return ApiRestaurantLister()
}
}

Now the RestaurantListViewController doesn’t know which implementation of RestaurantLister will be used.RestaurantListViewController and RestaurantLister are now loosely coupled. The dependency is now injected at runtime. All RestaurantListViewController really cares is to get a list of restaurants to display and that is it.

That is basic dependency injection.

What is Swinject and why use it?

Swinject is dependency injection framework. It does something similar to our previous example. However Swinject takes it one step further by leveraging Swift generics. Knowledge of Swift generics is not required. We will see some of examples further along this post.

In our previous example we have to write a function for each and every dependency we required. The example was simple and small. However apps can grow big and things can get complex easily. You could end having hundreds of dependencies. For which a function to create and inject its dependency would be required.

However by leveraging Swift generics, Swinject is capable of creating an easy way to resolve dependencies without having to create a specific function for each dependency to do so and then calling that named function. It resolves dependency in a consistent way. It also does so by using an interface which is highly readable or fluent.

It’s best to explore this by actually doing some coding and getting into it.

Using Swinject

In this section we will take an app project that already fetches and displays Restaurants; DLiver app.

DLiver app

We will start with an already existing app that does not use dependency injection. Then we will change the code to use dependency injection. Let’s download the starter project. Open terminal and run the following commands:

cd $HOME
curl -o DLiver.zip -L https://www.dropbox.com/s/fajqu5jx0tnzu4q/DLiver-starter.zip\?dl\=1 -s
unzip DLiver.zip
cd DLiver
open -a Xcode DLiver.xcodeproj

The app has a RestaurantLister that mimics fetching the list of restaurants from a server. It’s fetching restaurants from a file and returning that to the caller after some random time.

To this app we will:

  1. Change RestaurantLister into a protocol and the call the current implementation ServerRestaurantLister that conforms to such protocol
  2. Import Swinject
  3. Resolve dependencies at runtime using Swinject
  4. Resolve dependencies of storyboard view controllers using SwinjectStoryboard

1. Change RestaurantLister into a protocol

Let’s open RestaurantLister.swift. The file contains the RestaurantLister class which fetches the restaurants from the server. Here we want to create a protocol named RestaurantLister, however we can’t have two definitions the same.

Let’s first rename RestaurantLister class to ServerRestaurantLister. It is more specific and tells us from where the list of restaurants is fetched from.

Next within the same file add the following protocol:

protocol RestaurantLister {
func get(onCompletion: @escaping ([Restaurant]) -> ())
}

Next make ServerRestaurantLister conform to the RestaurantLister protocol:

class ServerRestaurantLister: RestaurantLister {

And finally open RestaurantListTableViewController.swift and change line 13 from:

private let restaurantLister = RestaurantLister()

to:

private let restaurantLister: RestaurantLister = ServerRestaurantLister()

In this first step we have laid the ground work to be able to have multiple implementations of RestaurantLister. However RestaurantListTableViewController is still creating the ServerRestaurantLister implementation. Let’s change that using dependency injection.

2. Import Swinject and Swinjectstoryboard

In this section we will simple import the Swinject framework to enable us to inject RestaurantLister implementations to the RestaurantListTableViewController using a fluent API.

We will import these frameworks using the Cocoapods. If you’re not familiar with Cocoapods worry not. It’s basically a tool to facilitate downloading and linking of iOS frameworks.

In the terminal run the following commands:

cd ~/DLiver
gem install cocoapods
cat > Podfile <<-EOF
target 'DLiver' do
use_frameworks!
pod 'Swinject', '2.7.1'
end
EOF
pod install

First we installed Cocoapods. Next we created a file that tells Cocoapods to fetch and install Swinject into our DLiver app. Lastly we told Cocoapods to carry out the fetching and installation of the frameworks based on our specification inside the Podfile.

You should have received a message on terminal such as the following:

Make sure to close any Xcode windows open and then on terminal run the following command:

open -a Xcode DLiver.xcworkspace

This will open the project in Xcode with Swinject installed in the DLiver app.

3. Resolve dependencies at runtime using Swinject

In this section we will create a container which upon request will deliver the class that conforms to a specific protocol.

Let’s open AppDelegate.swift. Here we will add the logic to register how dependencies should be resolved. First we have to make Swinject available to AppDelegate.swift. Right under import UIKit add the following line:

import Swinject

This will allow us access the code within Swinject.

Next inside AppDelegate class we will create and store an instance of Swinject’s Container to which we will tell how to resolve dependencies when asked. Create the following property inside AppDelegate:

var container = Container()

To tell Swinject how to resolve dependencies when asked for add the following function to AppDelegate class:

private func registerDependencies() {
self.container.register(RestaurantLister.self, factory: { resolver in
return ServerRestaurantLister()
})
}

Next we will call this function when the app has finished launching. Inside the function:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool 

call self.registerDependencies such that the function now looks like the following:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.registerDependencies()
return true
}

So far we have registered our RestaurantLister dependency. Next we have to inject it in RestaurantListTableViewController. Because our RestaurantListTableViewController is instantiated through storyboards we have no way to inject through the constructor. That leaves us with two other options on how to inject RestaurantLister: property or setter injection.

For this tutorial we’ll use property injection. Open RestaurantListTableViewController and change:

private let restaurantLister: RestaurantLister = ServerRestaurantLister()

to:

var restaurantLister: RestaurantLister!

Previously the property was inaccessible to external code to RestaurantListTableViewController (because it was private). We also expected the property to be set a instantiation and not modified through the class lifecycle hence we used let keyword.

Now the property will not be set at instantiation. We expect the property to be set after instantiation (var). Furthermore this property is required. RestaurantListTableViewController will not work without it. Hence we used the bang operator ! to specify that this property is not optional.

Run the app now. You should see the app crash.

We have registered RestaurantLister as a dependency and specified that RestaurantListTableViewController requires an instance of RestaurantLister to run. However we haven’t yet injected it into RestaurantListTableViewController. For such we have to wait for RestaurantListTableViewController to be loaded. When the app finished launch then Main.storyboard will loaded. At that point the app will call the AppDelegate didFinishLaunchingWithOptions function. We have already used this function to register the dependencies to be used by our app. After registering the dependencies add the following lines to inject ServerRestaurantLister into our RestaurantListTableViewController:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.registerDependencies()
let navigationController = self.window?.rootViewController as? UINavigationController
let restaurantListViewController = navigationController?.viewControllers[0] as? RestaurantListTableViewController
restaurantListViewController?.restaurantLister = container.resolve(RestaurantLister.self)
return true
}

Run the app. The app is now running with RestaurantLister being injected! 🎉

4. Resolve dependencies of storyboard view controllers using SwinjectStoryboard

In the previous section we already got the app running with dependency injection using Swinject. However the injection of the RestaurantLister into RestaurantListTableViewController is based on how our Main.storyboard is loaded.

Hypothetically let’s say we add a new screen to show after RestaurantListTableViewController, i.e. MenuViewController. We wouldn’t be able to inject MenuViewController’s dependencies when the app launches as the MenuViewController would only be instantiated after the user selects a restaurant in RestaurantListTableViewController.

We need a way to inject dependencies on view controllers loaded through storyboards after they have been instantiated.

Luckily for us SwinjectStoryboard offers functionality that extends Swinject to do so.

Before we can use SwinjectStoryboard we have to install it in the DLiver app. In Xcode open Podfile and right after the line pod 'Swinject', '2.7.1' add the following line:

pod 'SwinjectStoryboard', :git => 'https://github.com/anuragajwani/SwinjectStoryboard'

Next, close Xcode, open terminal and execute the following three commands:

cd ~/DLiver
pod install
open -a Xcode DLiver.xcworkspace

First we must instantiate Main.storyboard using SwinjectStoryboard. Open AppDelegate.swift. To enable us to use SwinjectStoryboard we must first import it. After import Swinject add the following line:

import SwinjectStoryboard

Next change the contents of func application(_ application: _, didFinishLaunchingWithOptions launchOptions:_) to:

self.registerDependencies()let window = UIWindow(frame: UIScreen.main.bounds)
window.makeKeyAndVisible()
self.window = window
let swinjectStoryboard = SwinjectStoryboard.create(name: "Main", bundle: nil, container: self.container)
window.rootViewController = swinjectStoryboard.instantiateInitialViewController()
return true

Above we have moved from the system loading Main.storyboard to SwinjectStoryboard loading it. This way SwinjectStoryboard knows when the view controllers are instantiated.

Next let’s add the function that does the injection of dependencies into view controllers once they have been instantiated. At the bottom of AppDelegate add the following function:

private func injectDependencies() {
self.container.storyboardInitCompleted(RestaurantListTableViewController.self) { (resolver, viewController) in
viewController.restaurantLister = resolver.resolve(RestaurantLister.self)
}
}

Lastly we have to call this function. After self.registerDependencies() in func application(_ application: _, didFinishLaunchingWithOptions launchOptions: _) -> Bool add the following line:

self.injectDependencies()

Run the app. You should see no visual difference from previously running the app. However now you have the ability to add as many view controllers to your storyboard without the need of changing how you inject dependencies when the flow of your app changes.

Summary

In this post we have learnt:

  • what is dependency injection
  • different ways of injecting dependencies
  • why use dependency injection
  • how to use dependency injection
  • how to use dependency injection using Swinject and SwinjectStoryboard

Final Notes

You can find the completed project here.

Stay tuned for more on iOS development! Follow me on Twitter or Medium.

--

--

Anurag Ajwani
Onfido Product and Tech

Senior iOS Engineer at Travelperk. 7+ years experience with iOS and Swift. Blogging my knowledge and experience.