/inject

Dependency Injection Container for Swift

Primary LanguageSwiftMIT LicenseMIT

inject

Swift Version Build Status CocoaPods Platform License

Inject is a dependency injection container for Swift that picks up the basic Spring ideas - as far as they are possible to be implemented - and additionally utilizes the Swift language features - e.g. closures - in order to provide a simple and intuitive api.

In addition to the core a number of other concepts are implemented

  • basic reflection and type introspection framework
  • configuration framework
  • logging & tracing framework
  • concurrency classes
  • xml parser
  • type conversion facilities

But let's come back to the dependency container again :-)

What's a dependency injection container anyway?

The basic idea is to have one central object that knows about all kind of different object types as well as object dependencies and whose task is to instantiate and assemble them appropriately by populating fields ( with property setters, methods or appropriate constructor calls ). Classes do not have to know anything about the current constallation - e.g. specific protocol implementation, or specific configuration values - as this know how is solely in the responsiblity of the container and injected into the classes.

If you think about unit testing, where service implementations need to be exchanged by some kind of local variants ( e.g. mocks ) you get a feeling for the benefits.

The other big benefit is that the lifecycle of objects is also managed by a central instance. This on the one hand avoids singleton patterns all over your code - which simply is a mess - and on the other hand allows for other features such as session scoped objects, or the possibility to shutdown the complete container - releasing ressources - with on call.

Features

Here is a summary of the supported features

  • specification of beans via a fluent interface or xml
  • full dependency management including cycle detection
  • all defintions are checked for typesafeness
  • integrated management of configuration values
  • injections resembling the spring @Inject autowiring mechanism
  • support for different scopes including singleton and protoype as builtin flavors
  • support for lazy initialized beans
  • support for bean templates
  • lifecycle methods ( e.g. postConstruct )
  • BeanPostProcessor's
  • FactoryBean's
  • support for hierarchical containers, that inherit beans ( and post processors )
  • support for placeholder resolution ( e.g. ${property=<default>}) in xml
  • support for custom namespace handlers in xml
  • automatic type conversions and number coercions in xml

Documentation

For detailed information please visit

  • The Wiki and
  • the generated API Docs
  • or simply play around with the included playground

Examples

Let's look at some simple examples.

let environment = try! Environment(name: "my first environment")

try! environment
   // a bar created by the default constructor
   
   .define(environment.bean(Bar.self, factory: Bar.init))
   
   // a foo that depends on bar
   
   .define(environment.bean(Foo.self, factory: {
            return Foo(bar: try! environment.getBean(Bar.self))
        }).requires(class: Bar.self))
        
   // get goin'
   
   .startup()

One the environment is configured, beans can simply be retrieved via the getBean() function.

let foo = try environment.getBean(Foo.self)

Behind the scenes all bean definitions will be validated - e.g. looking for cyclic dependencies or non resolvable dependencies - and all singleton beans will be eagerly constructed.

Other injections - here property injections - can be expressed via the fluent interface

environment.define(environment.bean(Foo.self, id: "foo-1")
   .property("name", value: "foo")
   .property("number", value: 7))

Injections

A similar concept as the Java @Inject annotations is available that let's you define injections on a class basis.

public class AbstractConfigurationSource : NSObject, Bean, BeanDescriptorInitializer, ... {
    // MARK: instance data
    
    ...
    var configurationManager : ConfigurationManager? = nil // injected
    
    ...
    
    // MARK: implement BeanDescriptorInitializer
    
    public func initializeBeanDescriptor(beanDescriptor : BeanDescriptor) {
        beanDescriptor["configurationManager"].inject(InjectBean())
    }
    
    // MARK: implement Bean
    
    // we know, that all injections have been executed....
    public func postConstruct() throws -> Void {
        try configurationManager!.addSource(self)
    }

The protocol BeanDescriptorInitializer can be implemented for thus purpose in order to add inejctions to properties. Valid values are:

  • InjectBean an injection for a specific object type
  • InjectConfigurationValue an injection of a configuration value

Scopes

Scopes determine when and how often a bean instance is created.

  • The default is "singleton", which will create an instance once and will cache the value.
  • "prototype" will recreate a new instance whenever a bean is requested.

Example:

environment.define(environment.bean(Foo.self)
   .scope("prototype")
   .property("name", value: "foo")
   .property("number", value: 7))

Other scopes can be simply added (e.g. session scope ) by defining the implementing class in the current environment.

Lazy Beans

Beans that are marked as lazy will be constructed after the first request.

Factory Beans

Factory beans are beans that implement a specific protocol and create other beans in turn.

environment
   .define(environment.bean(FooFactory.self)
      .property("someProperty", value: "...") // configure the factory....
      .target(Foo.self) // i will create foo's
    )
    
let foo = environment.getBean(Foo.self) // is created by the factory!

Abstract Beans

It is possible to define a bean skeletton - possibly hiding ugly technical parameters - and let the programmer finsh configuration by adding the missing parts:

environment
   // a template

   .define(environment.bean(Foo.self, id: "foo-template", abstract: true)
      .property("url", value: "...")
      .property("port", value: 8080))
   
   // the concrete bean
   
   .define(environment.bean(Foo.self, parent: "foo-template")
      .property("missing", value: "foo") // add missing properties
   )

Usually templates are part of a parent environment to separate technical aspects.

Bean Post Processor

Bean Post Processors are classes that implement a specific protocol and are called by the container in order to modify the to be constructed instance.

Lifecycle Callbacks

Different protocols can be implemenetd by classes which will be called by the container when an instance is created. The most important is a postConstruct that is called after the instance has been created and all psot processors have been executed.

Configuration Values

Every container defines a central registry that maintains configuration values - from different sources - that can be retrieved with an uniform api.

let environment = ...
environment.addConfigurationSource(ProcessInfoConfigurationSource()) // process info
environment.addConfigurationSource(PlistConfigurationSource(name: "Info")) // will read Info.plist in the current bundle

// retrieve some values

environment.getConfigurationValue(Int.self, key: "SIMULATOR_MAINSCREEN_HEIGHT", defaultValue: 100) // implicit conversion!

XML Configuration

And for all xml lovers ( :-) ), an xml parser for the original - at least a subset - spring schema.

<?xml version="1.0" encoding="UTF-8"?>
<beans
        xmlns:configuration="http://www.springframework.org/schema/configuration"
        xmlns="http://www.springframework.org/schema/beans"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
                        http://www.springframework.org/schema/configuration http://www.springframework.org/schema/util/spring-util.xsd">
    
    <!-- configuration values are collected from different sources and can be referenced in xml and the api -->
    <!-- in addition to static values dynamic sources are supported that will trigger listeners or simply change injected values on the fly-->
    
    <!-- here are some examples for sources -->
    
    <!-- builtin namespace & corresponding handler to set configuration values -->

    <configuration:configuration namespace="com.foo">
        <configuration:define key="bar" type="Int" value="1"/>
    </configuration:configuration>
    
    <!-- other supported sources are -->
    
    <!-- the current process info values, e.g. "PATH" -->
    
    <bean class="Inject.ProcessInfoConfigurationSource"/>
    
    <!-- a plist -->

    <bean class="Inject.PlistConfigurationSource">
        <property name="name" value="Info"/>
    </bean>
    
    <!-- a post processor will be called by the container after construction giving it the chance -->
    <!-- to modify it or completely exchange it with another object ( proxy... ) -->
    
    <!-- here we simple print every bean on stdout...:-) -->

    <bean class="SamplePostProcessor"/>

    <!-- create some foo's -->

    <!-- depends-on will switch the instantiation order -->

    <bean id="foo-1" class="Foo" depends-on="foo-2">
        <property name="id" value="foo-1"/>
        <!-- references an unknown configuration key, which will set the default value instead... -->
        <property name="number" value="${dunno=1}"/>
    </bean>

    <bean id="foo-2" class="Foo">
        <property name="id" value="foo-2"/>
        <!-- same thing recursively -->
        <property name="number" value="${dunno=${wtf=1}}"/>
    </bean>

    <!-- scope prototype means that whenever the bean is requestd a new instance will be created ( default scope is singleton ) -->
    <!-- other scopes yould be easily added, e.g. a session scope... -->

    <bean id="foo-prototype" class="Foo" scope="prototype">
        <property name="id" value="foo-prototype"/>
        <!-- this should work... the : separates the namespace from the key! -->
        <property name="number" value="${com.foo:bar}"/>
    </bean>

    <!-- bar will be injected by all foo's. Obviously the bar needs to be constructed first -->

    <bean id="bar-parent" class="Bar" abstract="true">
        <property name="magic" value="4711"/>
    </bean>

    <!-- will inherit the magic number -->
    <!-- lazy means that it will be constructed when requested for the first time -->

    <bean id="bar" class="Bar" parent="bar-parent" lazy="true">
        <property name="id" value="bar"/>
    </bean>

    <!-- both foo's will inject the bar instance -->

    <!-- baz factory will create Baz instances... -->

    <bean class="BazFactory" target="Baz">
        <property name="name" value="factory"/>
        <!-- will be set as the baz id... -->
        <property name="id" value="id"/>
    </bean>

    <!-- bazongs -->

    <bean id="bazong-1" class="Bazong">
        <property name="id" value="id"/>
        <!-- by reference -->
        <property name="foo" ref="foo-1"/>
    </bean>

    <bean id="bazong-2" class="Bazong">
        <property name="id" value="id"/>
        <!-- in-place -->
        <property name="foo">
            <bean class="Foo">
                <property name="id" value="foo-3"/>
                <property name="number" value="1"/>
            </bean>
        </property>
    </bean>
</beans>
var environment = Environment(name: "environment")
var data : NSData = ...
environment
   .loadXML(data)
   .startup()  

Logging

In addition to the injection container, a logging framework has been implemented - and integrated - as well.

Once the singleton is configured

// a composition of the different possible log entry constituents

let formatter = LogFormatter.timestamp("dd/M/yyyy, H:mm:s") + " [" + LogFormatter.logger() + "] " + LogFormatter.level() + " - " + LogFormatter.message()
let consoleLogger = ConsoleLog(name: "console", formatter: formatter, synchronize: false)

LogManager() 
           .registerLogger("", level : .OFF, logs: [QueuedLog(name: "console", delegate: consoleLogger)]) // root logger
           .registerLogger("Inject", level : .WARN) // will inherit all log destinations
           .registerLogger("Inject.Environment", level : .ALL)

the usual methods are provided

// this is usually a static var in a class!
var logger = LogManager.getLogger(forClass: MyClass.self) // will lookup with the fully qualified name

logger.warn("ouch!") // this is a autoclosure!

logger.fatal(SomeError(), message: "ouch")

The error and fatal functions are called with an ErrorType argument. Both functions will emit an message containing the original message, the error representation and the current stacktrace.

Provided log destinations are

  • console
  • file
  • rolling file log (logs get copied every day)
  • nslog
  • queued log destination

The queueded log destination uses a dispatch queue. As a default a serial queue will be created whose purpose simply is to serialize the entries. In this case ´synchronize: false´ prevents that the console operations are synchronized with a Mutex

Requirements

  • iOS 8.0+
  • OSX 10.9
  • WatchOS 2.0
  • TvOS 9.0
  • Xcode 7.0+

Installation

Cocoapods

To install with CocoaPods, add pod 'inject', '~> 1.0.2' to your Podfile, e.g.

target 'MyApp' do
  pod 'inject', '~> 1.0.2'
end

Then run pod install command. For details of the installation and usage of CocoaPods, visit its official website.

Limitations

Depending on the specific bean definition, it may be required that the corresponding classes derive from NSObject. This limitation is due to the - missing - Swift support for relection. As soon as the language evolves i would change that.

Roadmap

  • support more package managers
  • wait for replies :-)

License

This project is released under the MIT license. See LICENSE for details.