AnnotationInject

Swift dependency injection annotations. Using Sourcery and Swinject.

38
2
Swift

AnnotationInject

Cocoapods
SPM
tests
twitter

Generate your dependency injections. Aimed for safety.

AnnotationInject
🗽 Free you from manually registering your dependencies.
Spend less time to configure and more time to code!
🛡 No more runtime crash because dependency is not up-to-date. Everything is checked at compile-time.
👐 Based on open source tools you like as Sourcery and Swinject.
📖 100% open source under the MIT license

Documentation for a specific release might slightly differ. If you have troubles please check the release doc first (by selecting the release in Github switch branches/tags).

What’s the issue with injection?

Without annotations

Using a dependency injection library (say, Swinject) you need to remember to register your dependencies:

container.register(CoffeeMaker.self) { r in
  return CoffeeMaker(heater: r.resolve()!) // Trouble ahead, not sure Heater is in fact registered!
}

/// later in your code
let coffeeMaker = container.resolve(CoffeeMaker.self) // crash, missing Heater dependency!

Running this code we’ll get a crash at runtime: we didn’t register any heater, resulting in CoffeeMaker resolver to crash.

With annotations

Annotations will generate your dependencies and make sure everything resolves at compile time.

/// sourcery: inject
class CoffeeMaker {
    init(heater: Heater) {

    }
}

This time we’ll get a compile time error because we forgot to declare a Heater dependency. Houray!

Usage

1. Annotate your dependencies

/// sourcery: inject
class CoffeeMaker {
  init(heater: Heater) { }
}

/// sourcery: inject
class Heater {
    init() { }
}

2. Add a build phase to generate dependencies

See Installation for more details.

If not all dependencies can be resolved, the build phase will fail, preventing your code from compiling succesfully.

3. Add generated files and use generated code

let resolver = Assembler([AnnotationAssembly()]).resolver

// `registeredService` is generated code. It is completely safe at compile time.
let coffeeMaker = resolver.registeredService() as CoffeeMaker
let heater = resolver.registeredService() as Heater

Installation

Note: AnnotationInject depends/relies on Sourcery for annotations declaration, and Swinject as dependency injecter.

  • Swift Package Manager
dependencies: [
    .package(url: "https://github.com/pjechris/AnnotationInject.git", from: "0.6.0")
]

Then add a Build phases to your project:

swift run annotationinject-cli --sources <path to your sources> --output <path to output generated code> (--args imports=<MyLib1> -args imports=<MyLib2>>)
  • Swift Package Manager (Xcode)

Add AnnotationInject as dependency in Xcode then add this Build phase to your project:

SPM_CHECKOUT_DIR=${BUILD_DIR%Build/*}SourcePackages/checkouts/AnnotationInject
cd $SPM_CHECKOUT_DIR
/usr/bin/xcrun --sdk macosx swift run annotationinject-cli ...
  • CocoaPods

Add pod AnnotationInject to your Podfile and a new Build phases to your project:

"$(PODS_ROOT)"/AnnotationInject/Scripts/annotationinject --sources <path to your sources> --output <path to output generated code> (--args imports=<MyLib1> -args imports=<MyLib2>>)

Note: You can pass all sourcery command line options to annotationinject script.

  • Manually
  1. Install Swinject and Sourcery.

  2. Copy-paste Sources and Templates folders inside and add a new Build phases to your project:

sourcery --templates <path to copied templates> --sources <path to your sources> --output <path to output generated code> (--args imports=<MyLib1> -args imports=<MyLib2>>)

Available annotations

inject

Registers a class into the dependency container.

/// sourcery: inject
class CoffeeMaker { }
Generated code

container.register(CoffeeMaker.self) {
  return CoffeeMaker()
}

extension SafeDependencyResolver {
  func registeredService() -> CoffeeMaker {
    return resolve(CoffeeMaker.self)!
  }
}

Options

name
Define a name for the service. Generated method will use that name.
scope
See Swinject Object Scopes
type
Defines the type on which the class is registered. Use it when you want to resolve against a protocol.

/// sourcery:inject: scope = "weak", type = "Maker", name = "Arabica"
class CoffeeMaker: Maker { }

inject (init)

Registers a specific init for injection. If annotation is not provided, first found is used.

Note: Class still needs to be inject annotated.

// sourcery: inject
class CoffeeMaker {
  init(heater: Heater) { }

  // sourcery: inject
  convenience init() {
    self.init(heater: CoffeHeater())
  }
}
Generated code

container.register(CoffeeMaker.self) {
  return CoffeeMaker()
}

extension SafeDependencyResolver {
  func registeredService() -> CoffeeMaker {
    return resolve(CoffeeMaker.self)!
  }
}

inject (attribute)

Injects an attribute after init. Attribute requires to be marked as Optional (? or !).

Note: Class still needs to be inject annotated.

// sourcery: inject
class CoffeeMaker {
  /// sourcery: inject
  var heater: Heater!

  init() { }
}
Generated code

container.register(CoffeeMaker.self) {
  return CoffeeMaker()
}
.initCompleted { service, resolver in
  service.heater = resolver.registeredService()
}

provider

Uses a custom function to register your dependency. It is the same as implementing container.register manually while keeping safety.
Note that provided method must be called instantiate.

Note: If you’re providing 3rd party libraries (coming from Cocoapods for example), you will need to pass those imports to AnnotationInject using args.imports MyLib,MyLib2,... command line argument.

class CoffeeMaker {
  init(heater: Heater) { }
}

// sourcery: provider
class AppProvider {
  static func instantiate(resolver: SafeDependencyResolver) -> CoffeeMaker {
    return CoffeeMaker(heater: CoffeHeater())
  }
}
Generated code

container.register(CoffeeMaker, factory: AppProvider.instantiate(resolver:))

extension SafeDependencyResolver {
  func registeredService() -> CoffeeMaker {
    return resolve(CoffeeMaker.self)!
  }
}

provided (no longer needed with 0.5.0)

Declares a parameter as argument to define into the resolver method. Work on init and provider methods.

Caveats

Generated code does not compile because of missing imports

Set --args imports=<MyLib1> -args imports=<MyLib2>> so that generated code includes 3rd party libraries.

Foundation types (URLSession, NSNotificationCenter, …) are empty (.self) in generated code

Sourcery is not yet able to find those types. As such they are seen as non existent. Workaround: Define the surrounded type inside a Provider and give it foundation types.

Build phase is failing with no error reported

This might be coming from Sourcery having some incompatibilities with Xcode 11.4. Workaround: Install Sourcery using Homebrew then add to the build step SOURCERY_BINPATH=sourcery as environment variable.

Pods/Sourcery/bin/Sourcery.app/Contents/MacOS/Sourcery: No such file or directory

You’re probably using Sourcery as a Cocoapods dependency which unfortunately doesn’t always work well. Workaround: Install Sourcery using Homebrew then add to the build step SOURCERY_BINPATH=sourcery as environment variable.

License

This project is released under the MIT License. Please see the LICENSE file for details.