iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
😺

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 like if and for.
  • {# ... #}: 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
}
GitHubで編集を提案

Discussion