Skip to content

6.Sourcery AutoStringProperties

AliSoftware edited this page Sep 20, 2017 · 2 revisions

Classroom Walkthrough (Part 6)

Avoid repetitive code with custom template

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!

Step 6.1

  • Add a new phantom protocol to our SourceryProtocols.swift file so that we can use it to opt-in for that template
protocol AutoStringProperties {}
  • Annotate struct Person: AutoStringProperties and struct Address: AutoStringProperties so that they conform to that phantom protocol

We're now ready to create our template!

Step 6.2

  • Create a new file AutoStringProperties.stencil files in the CodeGenDemo/CodeGen/Templates folder
  • Copy-Paste in there the content of the extension Person declaring 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.swift file which has just been generated next to the AutoStringProperties.stencil file to see them both side by side

Step 6.3

  • Add this to the beginning of our AutoStringProperties.stencil template:
{% 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!

Step 6.4

{% 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! 👍

Step 6.5

You can see that inside a Stencil {% for … %} loop:

  • You can use where clauses, 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!

Step 6.6

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, the extension Person { … } that we copy-pasted before to have a good starting point)
  • Replace extension Person to use that {{ type.name }} that we wrote above: extension {{ type.name }}
  • Copy/Paste our inner {% for prop … %}…{% endfor ‰} Stencil loop inside the static let stringProperties, and the switch of the get & set of our subscript.
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!

Step 6.7

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 the static let stringProperties implementation, 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 the switch-es, adjust the code so it generate the case {{ forloop.counter }}: return self.{{ prop.name }} (get) or the case {{ forloop.counter }}: self.{{ prop.name }} = newValue (set).
  • Shoot! forloop.counter is 1-based, not 0-based! Ok, let's cheat a bit and change switch idx to switch idx+1 then!
  • 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 the default: case after our `{% endfor %}.

💡 The official Stencil documentation mentions the existence of forloop.counter0 which 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 %}

Step 6.8

  • 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.swift and Address.swift, as it's generated by Sourcery now

➡️ Next Steps

Clone this wiki locally