iTranslated by AI
Generating Swift Initializers with Sourcery
When defining things like DTOs in Swift, there are times when you want to define an initializer like the following:
public struct UserDTO {
public let userId: String
public let name: String
public let installationId: String
public let platform: String
public let version: Int
public init(userId: String, name: String, installationId: String, platform: String, version: Int) {
self.userId = userId
self.name = name
self.installationId = installationId
self.platform = platform
self.version = version
}
}
Even if you don't define an initializer for a Swift struct, a "memberwise initializer"—which takes the properties held by the type as arguments—is automatically defined, so it can be initialized. However, the access level of a memberwise initializer is the same as the properties if they are private or fileprivate, and internal in other cases. Therefore, if you want to initialize it from another module, you need to define a public initializer as shown above.
Writing this manually can be tedious when there are many properties. By using a library called Sourcery, we can have it automatically generated at build time when a struct is defined.
Sourcery
Sourcery is a library that generates Swift code according to defined template files. You execute it by specifying the source files, template files, and the output destination path as follows. Additionally, options can be omitted if written in .sourcery.yml.
$ ./sourcery --sources <sources path> --templates <templates path> --output <output path>
sources:
- <sources path>
- <sources path2>
templates:
<templates path>
output:
-<output path>
Defining the Protocol for Sourcery
First, define the protocol to be used with Sourcery.
protocol AutoInitializable { }
Writing Template Files Using Stencil
Template files are written using a template language called Stencil. Some template examples are available in the Sourcery repository, so you might be able to use them as-is depending on your needs: https://github.com/krzysztofzablocki/Sourcery/tree/master/Templates/Templates
For this initializer generation, we will write our own .stencil file.
Stencil's basic syntax consists of these three:
-
{{ ... }}: Allows you to define variables. -
{% ... %}: Referred to as "tags" in the documentation; these allow for control structures likeifandfor. -
{# ... #}: Allows you to write comments.
I wrote the following template file:
{% for type in types.implementing.AutoInitializable %}
{% map type.storedVariables into parameters using p %}{{p.name}}: {{p.typeName }}{% endmap %}
// sourcery:inline:{{ type.name }}.AutoInitializable
{{ type.accessLevel }} init({{parameters|join:", "}}) {
{% for parameter in type.storedVariables %}
self.{{ parameter.name }} = {{ parameter.name }}
{% endfor %}
}
// sourcery:end
{% endfor %}
The types on the first line is the Types type defined in Sourcery. By using types.implementing.ProtocolName, it returns a list of types in the source code that conform to the specified protocol. The variable type corresponds to the Type type defined in Sourcery.
On the second line, it retrieves the stored variable information (storedVariables) of the obtained type, extracts the parameter names and type names, and formats them into the parameter name: type name format.
Various other types are defined in Sourcery: https://cdn.rawgit.com/krzysztofzablocki/Sourcery/master/docs/Types.html
Also, while Stencil itself doesn't have a map feature, Sourcery seems to implement it using a library called StencilSwiftKit that extends Stencil's functionality.
From the third line onwards, I am writing the code I actually want to output, using the Type type and the information formatted in the second line.
Normally, files are generated as new files in the path specified by the output option, but since we want to generate initializers this time, we want to add the code generated from the template to the original source code.
// sourcery:inline:{{ type.name }}.AutoInitializable
// sourcery:end
Add the above two lines to the template and add comments as shown below to the part where you want the source code to be output. This allows the generated code to be inserted between the commented sections. Also, make the struct conform to the AutoInitializable protocol defined earlier so that the processing in the Stencil file can be executed.
public struct UserDTO: AutoInitiaizable {
public let userId: String
public let name: String
public let installationId: String
public let platform: String
public let version: Int
//sourcery:inline:AudienceInfo.AutoInitializable
//sourcery:end
}
Execution
By adding a script to run the sourcery command in Xcode's Build Phases, the following initializer will be added after building. By simply conforming to the protocol and adding the comments, you can now automatically generate the initializer.
public struct UserDTO: AutoInitiaizable {
public let userId: String
public let name: String
public let installationId: String
public let platform: String
public let version: Int
//sourcery:inline:AudienceInfo.AutoInitializable
public init(userId: String, name: String, installationId: String, platform: String, version: Int) {
self.userId = userId
self.name = name
self.installationId = installationId
self.platform = platform
self.version = version
}
//sourcery:end
}
Discussion