Cool Swift DI library in 200 code lines

--

EasyDi contains a dependency container for Swift. The syntax of this library was specially designed for rapid development and effective use.

It fits in 200 lines, thus can do everything you need for grown-up DI library:

- Objects creating with dependencies and injection of dependencies into existing ones
- Separation into containers - Assemblies
- Types of dependency resolution: objects graph, singleton, prototype
- Objects substitution and dependency contexts for tests

Here’s the syntax:

var apiClient: IAPIClient {
return define(init: APIClient()) {
$0.baseURl = self.baseURL
}
}

This makes it possible to resolve circular dependencies and implement already existing objects.

EasyDi is avaiable at Cocoapods and Github

It works with Swift 3 and 4, iOS 8+.

Example project is XKCD reader )

Why should i use DI and what is it?

Dependency inversion is very important if project contains more than 5 screens and will be supported for more than a year.
Here are three basic scenarios where DI makes life better:

Parallel development. One developer will be able to deal with UI, while another one will work with data if they agree in advance on the interfaces. Then UI can be developed with test data, and the data layer can be called from the test UI.

Tests. By substituting the network layer for objects with fixed responses, you can check all the options for UI behavior, including in case of errors.

Refactor. The network layer can be replaced with a new, fast version with a cache and another API, if you leave the protocol with the UI unchanged.

The essence of DI can be described in one sentence: Dependencies for objects should be closed by the protocol and passed to the object when creating from the outside.

// Instead of
class OrderViewController {
func didClickShopButton(_ sender: UIButton?) {
APIClient.sharedInstance.purchase(...)
}
}

// this approach should be used
protocol IPurchaseService {
func perform(...)
}

class OrderViewController {
var purchaseService: IPurchaseService?
func didClickShopButton(_ sender: UIButton?) {
self.purchaseService?.perform(...)
}
}

More details with the principle of dependency inversion and the SOLID concept can be found here(objc.io #15 DI) and here(Wikipedia. SOLID).

Dependency types

ObjectGraph

By default, all dependencies are resolved through the graph of the objects. If the object already exists on the stack of the current object graph, then it is used again. This allows to insert the same object into several objects, and also allow the cyclic dependencies. For example, take the objects A, B and C with links A-> B-> C. (Do not pay attention to RetainCycle).

class A {
var b: B?
}

class B {
var c: C?
}

class C {
var a: A?
}

This is how Assembly looks and here is a dependency graph.

class ABCAssembly: Assembly {

var a:A {
return define(init: A()) {
$0.b = self.B()
}
}

var b:B {
return define(init: B()) {
$0.c = self.C()
}
}

var c:C {
return define(init: C()) {
$0.a = self.A()
}
}
}

var a1 = ABCAssembly.instance().a
var a2 = ABCAssembly.instance().a
Two independent graphs were obtained.

Singleton

But it happens that you need to create one object, which will then be used everywhere, for example, the analytics system or the storage. It is not necessary to use the classic Singleton with SharedInstance, because it will not be possible to replace it. For these purposes, there is scope in EasyDi: singleton. This object is created once, once dependencies are introduced into it and more EasyDi does not change it, but only returns. For example, we make a singleton from B.

class ABCAssembly: Assembly {
var a:A {
return define(init: A()) {
$0.b = self.B()
}
}

var b:B {
return define(scope: .lazySingleton, init: B()) {
$0.c = self.C()
}
}

var c:C {
return define(init: C()) {
$0.a = self.A()
}
}
}

var a1 = ABCAssembly.instance().a
var a2 = ABCAssembly.instance().a
This time, one object graph was obtained, because B became common.

Prototype

Sometimes each request requires a new object. On the example of ABC objects for the A-prototype, this would look like this:

class ABCAssembly: Assembly {
var a:A {
return define(scope: .prototype, init: A()) {
$0.b = self.B()
}
}

var b:B {
return define(init: B()) {
$0.c = self.C()
}
}

var c:C {
return define(init: C()) {
$0.a = self.A()
}
}
}

var a1 = ABCAssembly.instance().a
var a2 = ABCAssembly.instance().a
There results that two graphs of objects give 4 copies of object A

It is important to understand that this is the entry point to the graph and it is not necessary to make prototypes from other dependencies. If you combine prototypes in a loop, then the stack will be overflow and the application will fall.

--

--