I would call it a very elaborated proof of concept, though we use it in production atm. We are open for every kind of feedback, improvements of the generated code/templates, discussion of the input format etc.
We propose a method to get rid of duplicate use of class values and namestrings, by generating explicit functions for registering and resolving. Doing this, we also can generate typed tuples to use when resolving, thus allowing better documented and less error-prone code.
We aim to support carthage in the near future
Add
pod 'Swinject-CodeGen', :git => 'https://github.com/Swinject/Swinject-CodeGeneration.git'
to your podfile.
- Define your dependencies in a .csv or .yml file (see below and example file)
- Add a call to generate the code as build script phase:
$PODS_ROOT/Swinject-CodeGen/bin/swinject_codegen -i baseInput.csv -o extensions/baseContainerExtension.swift
- Add the generated file (here:
extensions/baseContainerExtension.swift
) to xcode - Repeat if you need to support multiple targets/have multiple input files.
The code is then generated at every build run.
We love using swinject for dependency injection. However, we would like to stay to the DRY principle and really love types. When using swinject, lots of duplicate definitions appear, whenever we do a
container.register(PersonType.self, name: "initializer") { r in
InjectablePerson(pet: r.resolve(AnimalType.self)!)
}
let initializerInjection = container.resolve(PersonType.self, name:"initializer")!
the tuple (PersonType.self, name:"initializer") becomes very redundant across the code.
Furthermore, when using arguments, as done in
container.register(AnimalType.self) { _, name in Horse(name: name) }
let horse1 = container.resolve(AnimalType.self, argument: "Spirit") as! Horse
the argument: "Spirit"
part is not strictly typed when calling it.
We propose a solution to both these problems by using CodeGeneration
Input can be given as .csv or .yml
The call
./swinject_codegen -i example.csv -c
can be used to convert example.csv into example.csv.yml (also works for .yml).
Our basic csv structure is defined as follows:
SourceClassName; TargetClassName; Identifier; Argument 1 ... 9
The example above would translate to
PersonType; InjectablePerson; initializer
to generate both a registerPersonType_initializer
and a resolvePersonType_initializer
function.
See the examples below for more examples.
We decided to use ;
as delimiter instead of ,
to allow the use of tuples as types.
The ruby parser allows using //
and #
for comments.
Empty lines are ignored and can be used for grouping.
#ADD_DEPENDENCY <dependency>
and # ADD_DEPENDENCY <dependency>
can be used to specify dependencies, e.g. import KeychainAccess
Example for a .yml definition:
---
DEPENDENCIES:
- ADependency
DEFINITIONS:
- service: PersonType
component: InjectablePerson
name: initializer
- service: PersonType
component: InjectablePerson
- service: PersonType
component: PersonType
- service: AnotherPersonType
component: AnotherPersonType
- service: PersonType
component: InjectablePerson
arguments:
- argument_name: argument_name
argument_type: argument_type
- service: PersonType
component: InjectablePerson
arguments:
- argument_name: argument_name
argument_type: argument_type
- argument_name: argument_typewithoutspecificname
argument_type: argument_typeWithoutSpecificName
- argument_name: title
argument_type: String
- argument_name: string
argument_type: String
- service: PersonType
component: InjectablePerson
name: initializer
arguments:
- argument_name: argument_name
argument_type: argument_type
- argument_name: argument_typewithoutspecificname
argument_type: argument_typeWithoutSpecificName
- argument_name: title
argument_type: String
- argument_name: string
argument_type: String
PersonType
// this code is autogenerated, do not modify!
import Swinject
extension Resolvable {
func resolvePersonType() -> PersonType {
return self.resolve(PersonType.self)!
}
}
extension Container {
func registerPersonType(registerClosure: (resolver: ResolverType) -> (PersonType)) -> ServiceEntry<PersonType> {
return self.register(PersonType.self, factory: registerClosure)
}
}
PersonType; InjectablePerson
// this code is autogenerated, do not modify!
import Swinject
extension Resolvable {
func resolveInjectablePerson() -> InjectablePerson {
return self.resolve(PersonType.self) as! InjectablePerson
}
}
extension Container {
func registerInjectablePerson(registerClosure: (resolver: ResolverType) -> (InjectablePerson)) -> ServiceEntry<PersonType> {
return self.register(PersonType.self, factory: registerClosure)
}
}
PersonType; InjectablePerson; initializer
// this code is autogenerated, do not modify!
import Swinject
extension Resolvable {
func resolveInjectablePerson_initializer() -> InjectablePerson {
return self.resolve(PersonType.self, name: "initializer") as! InjectablePerson
}
}
extension Container {
func registerInjectablePerson_initializer(registerClosure: (resolver: ResolverType) -> (InjectablePerson)) -> ServiceEntry<PersonType> {
return self.register(PersonType.self, name: "initializer", factory: registerClosure)
}
}
PersonType; InjectablePerson; ; argumentName:ArgumentType
// this code is autogenerated, do not modify!
import Swinject
extension Resolvable {
func resolveInjectablePerson(argumentName argumentName: ArgumentType) -> InjectablePerson {
return self.resolve(PersonType.self, argument: argumentName) as! InjectablePerson
}
}
extension Container {
func registerInjectablePerson(registerClosure: (resolver: ResolverType, argumentName: ArgumentType) -> (InjectablePerson)) -> ServiceEntry<PersonType> {
return self.register(PersonType.self, factory: registerClosure)
}
}
If no explicit name is given, the lowercase type is used as argumentname.
PersonType; InjectablePerson; ; argumentName:ArgumentType; ArgumentTypeWithoutSpecificName; title:String; String
// this code is autogenerated, do not modify!
import Swinject
extension Resolvable {
func resolveInjectablePerson(argumentName argumentName: ArgumentType, argumenttypewithoutspecificname: ArgumentTypeWithoutSpecificName, title: String, string: String) -> InjectablePerson {
return self.resolve(PersonType.self, arguments: (argumentName, argumenttypewithoutspecificname, title, string)) as! InjectablePerson
}
}
extension Container {
func registerInjectablePerson(registerClosure: (resolver: ResolverType, argumentName: ArgumentType, argumenttypewithoutspecificname: ArgumentTypeWithoutSpecificName, title: String, string: String) -> (InjectablePerson)) -> ServiceEntry<PersonType> {
return self.register(PersonType.self, factory: registerClosure)
}
}
Example F: Different source and target with name with multiple arguments, both explicitly named and not
PersonType; InjectablePerson; initializer; argumentName:ArgumentType; ArgumentTypeWithoutSpecificName; title:String; String
// this code is autogenerated, do not modify!
import Swinject
extension Resolvable {
func resolveInjectablePerson_initializer(argumentName argumentName: ArgumentType, argumenttypewithoutspecificname: ArgumentTypeWithoutSpecificName, title: String, string: String) -> InjectablePerson {
return self.resolve(PersonType.self, name: "initializer", arguments: (argumentName, argumenttypewithoutspecificname, title, string)) as! InjectablePerson
}
}
extension Container {
func registerInjectablePerson_initializer(registerClosure: (resolver: ResolverType, argumentName: ArgumentType, argumenttypewithoutspecificname: ArgumentTypeWithoutSpecificName, title: String, string: String) -> (InjectablePerson)) -> ServiceEntry<PersonType> {
return self.register(PersonType.self, name: "initializer", factory: registerClosure)
}
}
Using the examples given at the beginning, we can now instead of
container.register(PersonType.self, name: "initializer") { r in
InjectablePerson(pet: r.resolve(AnimalType.self)!)
}
let initializerInjection = container.resolve(PersonType.self, name:"initializer")!
write:
container.registerPersonType_initializer { r in
InjectablePerson(pet: r.resolve(AnimalType.self)!)
}
let initializerInjection = container.resolvePersonType_initializer()
Also
container.register(AnimalType.self) { _, name in Horse(name: name) }
let horse1 = container.resolve(AnimalType.self, argument: "Spirit")
becomes
container.registerAnimalType { (_, name:String) in
Horse(name: name)
}
let horse1 = container.resolveAnimalType("Spirit")
We can provide more examples if desired.
The Integration is currently done by calling the following code in a very early (i.e. before compilation) build phase:
cd ${SRCROOT}/CodeGeneration/
swinject_codegen -i baseInput.csv -o extensions/baseContainerExtension.swift
swinject_codegen -i iosInput.csv -o extensions/iosContainerExtension.swift
swinject_codegen -i tvosInput.csv -o extensions/tvosContainerExtension.swift
The resulting extension files are added to xcode and given appropriate target settings.
The script also generates migration.sh files (when using the -m switch), which use sed to go through the code and replace simple cases (i.e. no arguments) of resolve and register. No automatic migration is available for cases with arguments, yet. Simply call the .sh file from the root of the project and compare the results in a git-GUI.
We currently use the code generation in a medium-sized app, with a total of ~130 lines of definitions across 3 .csv files (shared definitions, iOS only, tvOS only).
We found our code to become much more convenient to read and write, due to reduced duplication and autocompletion. We also have a much better overview the classes available through dependency injection.
Changing some definition immediately leads to information, where an error will occur.
We were able to replace all our occurences of .resolve(
and .register(
using the current implementation.
The main contributors for this are Daniel Dengler, David Kraus and Wolfgang Lutz.