/Dose

Swift - Dependency Injection Container

Primary LanguageSwiftMIT LicenseMIT

Dose

A Dependency Injection Container for Swift

Carthage compatible Build Status Licence MIT Swift 2.2 Platform

Some work is still in progress but it works well, any help will be appreciated.

Coming from the PHP world, I used to develop with a lot of frameworks and concepts and one thing I like is the Dependendy Injection Container and Services system of Symfony2

I didn't find out amazing "simple" DIC online except Typhoon which is great but also big. And I needed to play with Swift for a project.

Then here is Dose, a simple Dependency Injection Container clearly inspired by Symfony2 but also different and less powerful (at least for now)

Purposes

The objective here is to bring a DIC into an iOS application. All must stays simple, my goal was to avoid to always rely on the UIApplication delegate and always have plenty of singletons.

Key features

  • Easy to use
  • Parameter Injections
  • Service Injections
  • Closure Injections
  • Providers
  • Base on a configuration file (Plist for now)
  • Lazy loading of services (super important)
  • Pure Swift

Why it's cool and powerful

There is a bunch of useful reasons to use a service container:

  • to avoid singleton everywhere in your code
  • to improve the simplicity of your Units Tests
  • to decrease the couplage between the different classes of your code
  • to increase the legibility of your code
  • to allow you to organise you code by "service" and share those services!
  • Services will be created/instanciated only on demand wich is also a huge advantage for performances.

Simple explanation

First, really simple, base on the dependency injection principle. Let say you have a logger which respects the protocol LoggerProtocol

You can create your MyLogger : LoggerProtocol implementing the good methods, BUT, if you want later to change your Logger using antother library, well if this new lib is conform to the LoggerProtocol you have nothing to do except to make little change in the simple Plist configuration file!

Then if you have 5 services which each needs the logger, your code won't change. You'll just have to change the definition of the service.

And to call a method on one of your service, just call it:

Kernel.instance.get("my.service.name")->getLastFacebookComments()

Scroll down to see how to create a service.

Look a the DoseTest classes to understand better if you're not familiar with Dependency Injection Container

if you want to help me on the documentation, feel free to PR

Installation

Carthage

Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks.

To install the carthage tool on your system, please download and run the Carthage.pkg file for the latest release.

Alternatively only on Xcode 7.x, you can use Homebrew and install the carthage tool on your system simply by running brew update and brew install carthage. (note: if you previously installed the binary version of Carthage, you should delete /Library/Frameworks/CarthageKit.framework).

To integrate Dose into your Xcode project using Carthage, specify it in your Cartfile:

github "Plopix/Dose" "master"

"master" before the first 1.0.0 release.

Run carthage to build the framework and drag the built Dose.framework into your Xcode project.

Actually if you do not know how to use Carthage, please read here: Carthage - Get Started

Usage

Setting it up

A dose of a kernel

Yes, I called that the Kernel, firt you need to create it and to boostrap it.

The easiest way to get your Kernel available from where you want is to add a property on you application delegate

Note that it is not required to do that. That is just the quickiest way.

    @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {
        var window: UIWindow?
        /// Your Kernel
        let kernel = Kernel()
        
        func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        
            // Bootstrap your Kernel
            kernel.bootstrap()
            ....
            ....
        }
        

I would suggest to create your own Kernel by extending the Dose.Kernel

import Foundation
import Dose

class Kernel : Dose.Kernel {
    
    /**
    Sugar function to get the Kernel Instance
    Allow you to call Kernel.instance.get("service")
    */
    static var instance: Kernel {
        get {
            let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
            return appDelegate.kernel
        }
    }
}

Doing that, you'll be able in the future to override the boostrap() method if necessary.

A dose of a definition

The definition file is where all the magic is set up. You need a simple Plist file that looks like:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>parameters</key>
        <dict>
            <key>debug_output</key><true/>
            <key>lambdakey</key><string>A VALUE</string>
        </dict>
    	<key>services</key>
    	<dict>
        	<key>dose.service1</key>
        	<dict>
            	<key>class</key><string>DoseTests.Service1</string>
            	<key>arguments</key>
            	<array>
                	<string>@lambdakey</string>
                	<string>An argument</string>
	            </array>
    	    </dict>
	        <key>dose.service2</key>
    	    <dict>
        	    <key>class</key><string>DoseTests.Service2</string>
            	<key>arguments</key>
	            <array>
   		            <string>Second Service</string>
                    <string>@dose.service1</string>
	            </array>
        	</dict>
		</dict>
    </dict>
	</plist>
		

Find a bigger example here: https://github.com/Plopix/Dose/blob/master/DoseTests/services.plist

A dose of a service

Each service:

  • is define by an name. Ex:dose.service1

  • needs a "class" to be instanciated. Ex:DoseTests.Service1

    Please note that you need to precise the Prefix, it's usually you application name but it could be a class from another module.

  • can have several arguments.

A dose of an argument

The cool thing about argument is the "magic" character: "@". Whith that you can refer to another service!

Then in the previous example, dose.service2 will have dose.service1 passed in argument in order to be instanciated.

It works with the parameters too, because actually a parameter is not that much different than a service, one is a String or a Bool etc.. and the other is a Closure.

Amazing!

Creating a service

Of course, it is always your responsability to create and develop you service, but he MUST extend of Dose.Service. He will receive an Array as arguments. Here is an exemple:

    import Foundation
    import Dose

    class Service3 : Service {
        
        let name: String
        let logger: LoggerProtocol
        
        required init(args:[Any]) {
            name = args[1] as! String
            logger = args[0] as! LoggerProtocol
        }
    }

That's it, you are ready to go.

Retrieve a service

Just call it: Kernel.instance.get("dose.service.name")

Note that the call to a service trigger its instanciation.

Providers

  • @todo: Documentation, you can also read the code in Tests.

Todo

  • Check the circular references
  • Improve the plist features: tags? factory? inheritance?, scopes? etc..
  • More documentation
  • More documentation

Credits

Mostly inspired by Symfony2 Service Container, Silex and Pimple

Developed by SĂ©bastien Morel supported by Novactive

Contact, support, help, requests

Feel free to contact me: morel.seb[at]gmail[dot]com