A support package for Stitcher that defines meta programming utilities using Swift macros, enabling automatic parameter injection for functions and initializers as well as utilities for automatic dependency registration.
Contents:
- Stitcher Macros
StitcherMacros requires at least iOS 13, macOS 10.15, tvOS 13 or watchOS 6 and Swift version 5.9.
- Automatic generation of functions and initializers with injected parameters providing an easy way to implement parameter injection.
- Automatic generation of registrations for dependencies.
- Automatic generation of registered dependency groups.
StitcherMacros will automatically install and export Stitcher when installed.
You may add Stitcher as a Swift Package dependency using Xcode 11.0 or later, by selecting File > Swift Packages > Add Package Dependency...
or File > Add packages...
in Xcode 13.0 and later, and adding the url below:
https://github.com/athankefalas/StitcherMacros.git
You may also install this library manually by downloading the StitcherMacros
project and including it in your project.
A few of the items discussed in this section are predicated to being familiar with the basic concepts defined in the Stitcher library.
Parameter injection refers to the practice of injecting the dependencies of a type as arguments in it's initializer or when invoking a function. In order to automatically generate initializers or functions, and having their parameters automatically injected by Stitcher
, the targeted init
or func
declaration must be attributed with the @InjectedParameters
macro.
class UploadService {
private let networkingSession: NetworkingSession
@InjectedParameters
init(networkingSession: NetworkingSession) {
self.networkingSession = networkingSession
}
}
Which will be expanded to:
class UploadService {
private let networkingSession: NetworkingSession
@InjectedParameters
init(networkingSession: NetworkingSession) {
self.networkingSession = networkingSession
}
/// Automatically generated convenience initializer
convenience init() {
self.init(
networkingSession: try! DependencyGraph.inject(
byType: NetworkingSession.self
)
)
}
}
Similarly the same pattern can be used with a function with some, or all of the parameters injected:
class UploadService {
@InjectedParameters(ignoring: "file")
func upload(file: URL, networkingSession: NetworkingSession) {}
}
Which will expand to the following:
class UploadService {
@InjectedParameters(ignoring: "file")
func upload(file: URL, networkingSession: NetworkingSession) {}
/// Automatically generated function when macro is expanded:
func upload(file: URL) {
upload(
file: file,
networkingSession: try! DependencyGraph.inject(
byType: NetworkingSession.self
)
)
}
}
The InjectedParameters
macro can be configured with the following arguments:
-
parent
The kind of the parent type. This is used when defining initializers with injected parameters to determine the kind of initializer to create. By default, this option is set to a class parent which will create a convenience initializer. When using the macro on functions this argument is ignored.
-
strategy
The strategy used to inject dependencies in the generated initializer - function.
-
ignoring
The names of any ignored parameters that will not be injected and will instead be manually passed by the caller of the generated initializer - function. By default, none of the parameters present in the original declaration are ignored.
The parent kind argument is used to control the macro output when using it with type initializers. For Swift versions <=
5.10, the compiler plugin engine does not provide information of the attached parent type for peer macros such as InjectedParameters
. Consequently, when adding this macro to struct
or enum
initializers, in Swift versions older than 6, the parent type kind must also be provided to avoid compile time errors in the generated code.
Definition of acceptable parent kind values:
/// The kind of parent an attached peer macro has.
public enum AttachedParentKind {
case actorParent
case classParent
case enumParent
case structParent
}
The default value of this argument is .classParent
, which is compatible with both class
and actor
types as they are both reference types and secondary initializers can be defined as convenience
initializers in the same way.
Using InjectedParameters
on a struct initializer:
struct Service {
let repository: EntityRepository
@InjectedParameters(parent: .structParent)
init(repository: EntityRepository) {
self.repository = repository
}
}
The strategy parameter controls how the dependency will be injected, either by type or by name. By default, all dependencies in generated code will be injected using the .stitcherByType
strategy.
class Service {
@InjectedParameters(strategy: .stitcherByType)
func findAll(in repository: EntityRepository) -> [Entity] { [] }
/// Automatically generated function when macro is expanded:
func findAll() -> [Entity] {
findAll(
in: try! DependencyGraph.inject(
byType: EntityRepository.self
)
)
}
@InjectedParameters(strategy: .stitcherByType)
func clear(_ repository: EntityRepository) {}
/// Automatically generated function when macro is expanded:
func clear() {
clear(
try! DependencyGraph.inject(
byType: EntityRepository.self
)
)
}
}
The .stitcherByName
strategy may be used instead, in order to use injection by name. The name used during injection is the parameter name used in the original declaration.
class Service {
@InjectedParameters(strategy: .stitcherByName)
func findAll(in repository: EntityRepository) -> [Entity] { [] }
/// Automatically generated function when macro is expanded:
func findAll() -> [Entity] {
findAll(
in: try! DependencyGraph.inject(
byName: "repository"
)
)
}
@InjectedParameters(strategy: .stitcherByName)
func clear(_ repository: EntityRepository) {}
/// Automatically generated function when macro is expanded:
func clear() {
clear(
try! DependencyGraph.inject(byName: "repository")
)
}
}
The ignored parameter is a sequence of parameter names, that controls which of the initializer - function parameters will be ignored by the macro and be manually injected when invoking it.
class Service {
@InjectedParameters(
ignoring: "imageData", "attributes"
)
func upload(
imageData: Data,
convertedWith attributes: ImageAttributes = [],
processingService: ImageProcessingService
) async throws {}
/// Automatically generated function when macro is expanded:
func upload(
imageData: Data,
convertedWith attributes: ImageAttributes = []
) async throws {
try await upload(
imageData: imageData,
convertedWith: attributes,
processingService: try! DependencyGraph.inject(
byType: ImageProcessingService.self
)
)
}
}
The @InjectedParameters
macro supports all initializers - functions with non-generic parameters. If an initializer - function has a generic parameter and injection is required for the rest of the parameters, the generic parameter must be explicitly ignored using the ignored
parameter.
Please note, that all generated initializers - functions are marked with the @_disfavoredOverload
attribute and have a lower invocation priority than explicitly declared variants. Furthermore, a generated initializer that is also supported as the preferred initializer for the type, is automatically attributed with the @PreferredInitializer
macro.
The @PreferredInitializer
macro is a marker attribute that declares which initializer should be used during automatic dependency registration generation by the @Registerable
macro.
The preferred initializer supports any initializer that has no generic aguments, is not failable either by returning nil or throwing an error and is synchronous.
The @Registerable
macro can be used to automatically generate a dependency registration and attach it as a static member to the specified type, by conforming to the RegisterableDependency
protocol.
@Registerable
class Service {
init() {}
}
// Expanded macro:
extension Service: RegisterableDependency, AutoregisterableDependency {
static let dependencyRegistration = GeneratedDependencyRegistration<Service>(
locator: .type(Service.self),
scope: .automatic(for: Service.self),
eagerness: .lazy
) {
Service()
}
}
Furthermore, if the dependency is located by name or by an associated value the locator raw value is also automatically added to the conformance:
@Registerable(by: .name("service"))
class Service {
init() {}
}
// Expanded macro:
extension Service: RegisterableDependency, AutoregisterableDependency {
static let dependencyName: String = "service"
static let dependencyRegistration = GeneratedDependencyRegistration<Service>(
locator: .name("service"),
scope: .automatic(for: Service.self),
eagerness: .lazy
) {
Service()
}
}
At runtime, the registraton can be added to a dependency container as follows:
@main
struct SomeApp: App {
@Dependencies
var container = DependencyContainer {
// Dependency registration
Service.dependencyRegistration
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
The configuration of the @Registerable
macro can be configured with the same options as the basic dependency registration using the Dependency
struct.
Controls the way the dependency will be located. By default, the dependency will be located by it's type.
Controls the scope of the dependency. By default, the dependency scope that will be used is .automatic()
.
Controls the eagerness of the dependency. By defaultm the dependency eagerness that will be used is .lazy
.
The @PreferredInitializer
macro can be used to disambiguate which initializer will be selected for instantiating the dependency. In cases where multiple preferred initializers are found, the initializer with the smallest count of parameters will be selected.
Stitcher Macros includes a stencil template that can be optionally used with Sourcery in order to generate a dynamic dependency group at build time from all dependencies that are attributed with the @Registerable
macro.
Furthermore, a completely custom stencil template can be used instead, either by targeting the @Registerable
macro attribute or the AutoregisterableDependency
protocol.
- Follow the steps in the Sourcery repository to install the command line tool or package plugin.
- Create a sourcery configuration file. A simple example configuration file can be found at
Sourcery/sourcery.yml
. - Add Sourcery as a build phase script or a commit phase triggered script.
- Add the autogenerated dependency group in a
DependencyContainer
to register the dependencies at runtime.
@main
struct SomeApp: App {
@Dependencies
var container = DependencyContainer {
// Autogenerated Dependency registrations
DependencyGroup.autoregisteredDependencies
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
The Sourcery
integration is optional and the provided template and configuration are meant to serve as easy launch points, therefore it is recommended that they are adapted to better fit the build phase / pipeline of the specific project.
If you have a problem with the library, or have a feature request please make sure to open an issue.