-
Notifications
You must be signed in to change notification settings - Fork 2
6.Sourcery AutoStringProperties
The PersonRecordViewController is a UITableViewController which uses an trick (ugly, but at least serves a good example for our purpose) to access properties to display in each cell using indexes.
Note how Person.swift and Address.swift both declare a static let stringProperties and a subscript(propertyIndex idx: Int) -> String — which the UITableViewDataSource uses to access properties count & label and their values by index.
You can already guess how maintenance of those could be a nightmare in case we later add more properties. So let's code-generate those too!
- Add a new phantom protocol to our
SourceryProtocols.swiftfile so that we can use it to opt-in for that template
protocol AutoStringProperties {}- Annotate
struct Person: AutoStringPropertiesandstruct Address: AutoStringPropertiesso that they conform to that phantom protocol
We're now ready to create our template!
- Create a new file
AutoStringProperties.stencilfiles in theCodeGenDemo/CodeGen/Templatesfolder - Copy-Paste in there the content of the
extension Persondeclaring the code we want to generate. We'll use that as a starting point for the content of our template.
extension Person {
static let stringProperties: [String] = [
L10n.Person.firstName,
L10n.Person.lastName,
]
subscript(propertyIndex idx: Int) -> String {
get {
switch idx {
case 0: return self.firstName
case 1: return self.lastName
default: fatalError("Out of bounds")
}
}
set {
switch idx {
case 0: self.firstName = newValue
case 1: self.lastName = newValue
default: fatalError("Out of bounds")
}
}
}
}- In the terminal, Start Sourcery's daemon/watch mode:
$ sourcery-0.8.0/bin/sourcery --watch- In your favorite text editor, open the
AutoStringProperties.generated.swiftfile which has just been generated next to theAutoStringProperties.stencilfile to see them both side by side
- Add this to the beginning of our
AutoStringProperties.stenciltemplate:
{% for type in types.implementing.AutoStringProperties %}
- {{ type.name}}
{% endfor %}
We can see it properly generates the list of Address & Person. Our first {% for %} loop works!
- Look at the documentation for the meta-properties Sourcery exposes for each type
- We'll use the
storedVariablesmeta-property of each type to list all the properties of our structs. Try modifying your template like this to list every property of each of our conforming types:
{% for type in types.implementing.AutoStringProperties %}
- {{ type.name}}
{% for prop in type.storedVariables %}
-> {{ prop.name }}
{% endfor %}
{% endfor %}
And look at the generated code. Good! We're moving forward! 👍
You can see that inside a Stencil {% for … %} loop:
- You can use
whereclauses, just like in Swift - You have access to special variables like
forloop.counter. We could use that for determine our indexes
Also, you can see [in the Sourcery documentation] that each Variable has a typeName property (of meta-type TypeName) which in turn exposes its name. We can use that to only select String properties.
- Modify the beginning of your template (our scratchpad section) like this:
{% for type in types.implementing.AutoStringProperties %}
- {{ type.name}}
{% for prop in type.storedVariables where prop.typeName.name == "String" %}
-> {{ forloop.counter }}: {{ prop.name }}
{% endfor %}
{% endfor %}
Save and look at the generated code. It seems we have everything we need now!
Let's mix all that to make it look like what we want:
- Move the last
{% endfor %}so that it wraps the structure of code we want to generate (remember, theextension Person { … }that we copy-pasted before to have a good starting point) - Replace
extension Personto use that{{ type.name }}that we wrote above:extension {{ type.name }} - Copy/Paste our inner
{% for prop … %}…{% endfor ‰}Stencil loop inside thestatic let stringProperties, and theswitchof theget&setof oursubscript.
Here's what the template should look like at that step
{% for type in types.implementing.AutoStringProperties %}
extension {{ type.name}} {
static let stringProperties: [String] = [
{% for prop in type.storedVariables where prop.typeName.name == "String" %}
-> {{ forloop.counter }}: {{ prop.name }}
{% endfor %}
L10n.Person.firstName,
L10n.Person.lastName,
]
subscript(propertyIndex idx: Int) -> String {
get {
switch idx {
{% for prop in type.storedVariables where prop.typeName.name == "String" %}
-> {{ forloop.counter }}: {{ prop.name }}
{% endfor %}
case 0: return self.firstName
case 1: return self.lastName
default: fatalError("Out of bounds")
}
}
set {
switch idx {
{% for prop in type.storedVariables where prop.typeName.name == "String" %}
-> {{ forloop.counter }}: {{ prop.name }}
{% endfor %}
case 0: self.firstName = newValue
case 1: self.lastName = newValue
default: fatalError("Out of bounds")
}
}
}
}
{% endfor %}
We're real close now!
Focus on the code generated for Person, since that's what we took for initial inspiration. See how close we are.
- In the
{% for %}loop we copied in thestatic let stringPropertiesimplementation, adjust the code so it only generate the list of{{ prop.name }}. Don't forget to add quotes and the comma to separate items! - In the
{% for %}loop we copied inside theswitch-es, adjust the code so it generate thecase {{ forloop.counter }}: return self.{{ prop.name }}(get) or thecase {{ forloop.counter }}: self.{{ prop.name }} = newValue(set). - Shoot!
forloop.counteris 1-based, not 0-based! Ok, let's cheat a bit and changeswitch idxtoswitch idx+1then! - Then remove the hard-coded
case …that we had before (the ones that we used for inspiration to see what we wanted our code to look like) to only keep thedefault:case after our `{% endfor %}.
💡 The official Stencil documentation mentions the existence of
forloop.counter0which is 0-based, but Sourcery 0.8.0 doesn't include the release of Stencil containing that new feature, which was added only recently to Stencil.
Here's what our template should look like now
{% for type in types.implementing.AutoStringProperties %}
extension {{ type.name}} {
static let stringProperties: [String] = [
{% for prop in type.storedVariables where prop.typeName.name == "String" %}
L10n.{{ type.name }}.{{ prop.name }},
{% endfor %}
]
subscript(propertyIndex idx: Int) -> String {
get {
switch idx+1 {
{% for prop in type.storedVariables where prop.typeName.name == "String" %}
case {{ forloop.counter }}: return self.{{ prop.name }}
{% endfor %}
default: fatalError("Out of bounds")
}
}
set {
switch idx+1 {
{% for prop in type.storedVariables where prop.typeName.name == "String" %}
case {{ forloop.counter }}: self.{{ prop.name }} = newValue
{% endfor %}
default: fatalError("Out of bounds")
}
}
}
}
{% endfor %}
- Add the new generated file to the
CodeGenDemo/CodeGen/Generated/group of your Xcode project - Remove the old code that was hand-written from
Person.swiftandAddress.swift, as it's generated by Sourcery now