Dependency Injection for iOS in Swift
A powerful concept in modern software design is dependency injection [1]. Basically, it holds that an instance should never create any dependencies internally, but rather provide an interface that allows for installation or injection of relationships from outside of the object. For example, in the code below an instance of class Foo
is held inside of class Bar
, but class Bar
has taken on the responsibility of creating the instance.
class Foo {}
class Bar {
private var foo: Foo
init() {
self.foo = Foo()
}
}
Though this is fine, the internal construction offers no way to modify the construction behavior of class Foo
. Moreover, there is no way to provide a substitute or mock of class Foo
which would be very useful when testing the behavior of class Bar
. The easiest and safest way to allow this is to remove the construction of Foo
from inside Bar
and instead provide an already-constructed instance of Foo
in the constructor of Bar
class Foo {}
class Bar {
private var foo: Foo
init(foo: Foo) {
self.foo = foo
}
}
Now we are free to provide whatever we want for Foo
in the constructor.
Unfortunately, this does not work well for iOS views and controllers that are designed in Xcode storyboards and instantiated from bundles when the application runs. The first time a controller is available for manipulation, the iOS platform has already created the instances – there just is no easy way to provide additional values to the constructor at run-time.
There are some libraries around that attempt to overcome these limitations. One in particular, Dip appears to do a really nice job. However, for what I am working on now, I thought it overkill.
For views, we need to inject dependencies into their view controller before they appear. The normal way to do so is in the prepare(for segue:,sender:)
method of the current view controller. This works great, but we still need something for the initial view. The best choice is to rely on the application delegate, which has control prior to showing the main view of the application.
Protocols / Interfaces
In my app, there are four protocols/interfaces that manage specific functionalities:
UserSettingsInterface
– settings that the user can set and modify to affect the behavior of the applicationRecordingsStoreInterface
– manages the creation, updating, and deletion ofRecording
instances via Core DataDropboxControllerInterface
– handles uploading of data after a recordingRecordingActivityLogicInterface
- contains the “business logic” involved in performing a recording
The application delegate creates instances of these and holds strong references to them so that they will remain alive for the duration of the application.
Each of the above protocols has an implementation that provides the functionality for the application. The use of protocols however allows us to create synthetic implementations during testing. A test harness creates mock objects that implement the above protocols/interfaces, but which act and respond in a well-defined manner using synthetic data and actions since the goal is to exercise the code under test.
Here is the protocol for the DropBoxController
:
public protocol DropboxControllerInterface: class {
func toggleAccountLinking(viewController: UIViewController)
}
It only defines one method, to enable/disable account linking in the application. There is also an associated dependent protocol – DropboxControllerDependent
– which entities in the app can implement to announce that they depend on a DropBoxControllerInterface
object.
public protocol DropboxControllerDependent: class {
var dropboxController: DropboxControllerInterface! { get set }
}
Though the actual DropboxController
class is filled with functionality to work with the Dropbox cloud, the application only needs to work with toggleAccountLinking
to gain Dropbox functionality. The protocol effectively hides or abstracts away implementation details for us.
Injecting Dependencies
The main view of my app contains a UITabBar
inside of which reside three other views. The tab bar controller (TabBarController
) does not have any custom dependencies, but the views that it contains do, so we must navigate the view controller hierarchy to find those. Here is the code that does just that:
guard let rvc = window!.rootViewController as? TabBarController else {
fatalError("expected TabBarController as first view controller")
}
rvc.childViewControllers.forEach { viewController in
let injector = { (viewController: AnyObject) in
if let tmp = viewController as? UserSettingsDependent {
tmp.userSettings = userSettings
}
if let tmp = viewController as? RecordingsStoreDependent {
tmp.recordingsStore = recordingsStore
}
if let tmp = viewController as? DropboxControllerDependent {
tmp.dropboxController = dropboxController
}
if let tmp = viewController as? RecordingActivityLogicDependent {
tmp.recordingActivityLogic = recordingActivityLogic
}
}
if let tmp = viewController as? UINavigationController {
injector(tmp.topViewController!)
}
else {
injector(viewController)
}
}
We iterate over the view controllers held by the tab bar controller and we invoke injector()
on each to install any declared dependencies. Note that if the controller is a UINavigationController
we grab the controller that is the top one since that is ours.
Here we see the dependent protocols for each of the interface protocols mentioned earlier. If a controller implements a dependent protocol, then it has a property which will accept an object that implements the associated interface protocol. Note that this kind of logic is what a package like Dip will do for you automatically.
Segue Injection
Another point of injection for view controllers is the prepare(for segue:…)
method of a current UIViewController
object. This method runs just before a new view appears on the screen, and the current view controller gets a chance to forward pertinent information to the new view controller. My application currently does not have any segues as all of the views appear from tab bar activity.
Image adapted from one found at nearsoft’s “Dependency Injection in AngularJS” ↩