Swinject-CodeGen provides a method to get rid of duplicate use of class values and namestrings, by generating explicit functions for registering and resolving using Swinject. Doing this, we also can generate typed tuples to use when resolving, thus allowing better documented and less error-prone code.
Add
pod 'Swinject-CodeGen'
to your podfile.
Add
github "Swinject/Swinject-CodeGeneration"
to your Cartfile.
- 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:
For Cocoapods:
$PODS_ROOT/Swinject-CodeGen/bin/swinject_codegen -i baseInput.csv -o extensions/baseContainerExtension.swiftFor Carthage:
$SRCROOT/Carthage/Checkouts/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.
When using Swinject, lots of duplicate definitions appear, whenever we do a
container.register(PersonType.self, name:"initializer"){ r inInjectablePerson(pet: r.resolve(AnimalType.self)!)}letinitializerInjection= 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 inHorse(name: name)}lethorse1= container.resolve(AnimalType.self, argument:"Spirit")as!Horsethe 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 ... 9The example above would translate to
PersonType; InjectablePerson; initializerto 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.
#= <header> can be used to specify additional lines, e.g. #= import KeychainAccess
When using typed dictionaries or arrays as parameters, use Array<Type> instead of [Type] and Dictionary<TypeA, TypeB> instead of [TypeA:TypeB]:
PersonType; InjectablePerson; initializer; additionalNames:Array<String>; family:Dictionary<String,String>;Example for a .yml definition:
---
HEADERS:
- import ADependencyDEFINITIONS:
- service: PersonTypecomponent: InjectablePersonname: initializer
- service: PersonTypecomponent: InjectablePerson
- service: PersonTypecomponent: PersonType
- service: AnotherPersonTypecomponent: AnotherPersonType
- service: PersonTypecomponent: InjectablePersonarguments:
- argument_name: argument_nameargument_type: argument_type
- service: PersonTypecomponent: InjectablePersonarguments:
- argument_name: argument_nameargument_type: argument_type
- argument_name: argument_typewithoutspecificnameargument_type: argument_typeWithoutSpecificName
- argument_name: titleargument_type: String
- argument_name: stringargument_type: String
- service: PersonTypecomponent: InjectablePersonname: initializerarguments:
- argument_name: argument_nameargument_type: argument_type
- argument_name: argument_typewithoutspecificnameargument_type: argument_typeWithoutSpecificName
- argument_name: titleargument_type: String
- argument_name: stringargument_type: StringPersonType// this code is autogenerated, do not modify!
import Swinject
extensionResolver{func resolvePersonType()->PersonType{returnself.resolve(PersonType.self)!
}}extensionContainer{@discardableResultfunc registerPersonType(registerClosure:@escaping(_ resolver:Resolver)->(PersonType))->ServiceEntry<PersonType>{returnself.register(PersonType.self, factory: registerClosure)}}PersonType; InjectablePerson// this code is autogenerated, do not modify!
import Swinject
extensionResolver{func resolveInjectablePerson()->InjectablePerson{returnself.resolve(PersonType.self)as!InjectablePerson}}extensionContainer{@discardableResultfunc registerInjectablePerson(registerClosure:@escaping(_ resolver:Resolver)->(InjectablePerson))->ServiceEntry<PersonType>{returnself.register(PersonType.self, factory: registerClosure)}}PersonType; InjectablePerson; initializer// this code is autogenerated, do not modify!
import Swinject
extensionResolver{func resolveInjectablePerson_initializer()->InjectablePerson{returnself.resolve(PersonType.self, name:"initializer")as!InjectablePerson}}extensionContainer{@discardableResultfunc registerInjectablePerson_initializer(registerClosure:@escaping(_ resolver:Resolver)->(InjectablePerson))->ServiceEntry<PersonType>{returnself.register(PersonType.self, name:"initializer", factory: registerClosure)}}PersonType; InjectablePerson; ; argumentName:ArgumentType// this code is autogenerated, do not modify!
import Swinject
extensionResolver{func resolveInjectablePerson(argumentName:ArgumentType)->InjectablePerson{returnself.resolve(PersonType.self, argument: argumentName)as!InjectablePerson}}extensionContainer{@discardableResultfunc registerInjectablePerson(registerClosure:@escaping(_ resolver:Resolver, _ argumentName:ArgumentType)->(InjectablePerson))->ServiceEntry<PersonType>{returnself.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
extensionResolver{func resolveInjectablePerson(argumentName:ArgumentType, argumenttypewithoutspecificname:ArgumentTypeWithoutSpecificName, title:String, string:String)->InjectablePerson{returnself.resolve(PersonType.self, arguments: argumentName, argumenttypewithoutspecificname, title, string)as!InjectablePerson}}extensionContainer{@discardableResultfunc registerInjectablePerson(registerClosure:@escaping(_ resolver:Resolver, _ argumentName:ArgumentType, _ argumenttypewithoutspecificname:ArgumentTypeWithoutSpecificName, _ title:String, _ string:String)->(InjectablePerson))->ServiceEntry<PersonType>{returnself.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
extensionResolver{func resolveInjectablePerson_initializer(argumentName:ArgumentType, argumenttypewithoutspecificname:ArgumentTypeWithoutSpecificName, title:String, string:String)->InjectablePerson{returnself.resolve(PersonType.self, name:"initializer", arguments: argumentName, argumenttypewithoutspecificname, title, string)as!InjectablePerson}}extensionContainer{@discardableResultfunc registerInjectablePerson_initializer(registerClosure:@escaping(_ resolver:Resolver, _ argumentName:ArgumentType, _ argumenttypewithoutspecificname:ArgumentTypeWithoutSpecificName, _ title:String, _ string:String)->(InjectablePerson))->ServiceEntry<PersonType>{returnself.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 inInjectablePerson(pet: r.resolve(AnimalType.self)!)}letinitializerInjection= container.resolve(PersonType.self, name:"initializer")!write:
container.registerPersonType_initializer{ r inInjectablePerson(pet: r.resolve(AnimalType.self)!)}letinitializerInjection= container.resolvePersonType_initializer()Also
container.register(AnimalType.self){ _, name inHorse(name: name)}lethorse1= container.resolve(AnimalType.self, argument:"Spirit")becomes
container.registerAnimalType{(_, name:String)inHorse(name: name)}lethorse1= container.resolveAnimalType("Spirit")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 two medium-sized apps across tvOS and iOS.
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 occurrences of .resolve( and .register( using the current implementation.
The original idea for combining CodeGeneration and Swinject came from Daniel Dengler, David Kraus and Wolfgang Lutz.