Using Template Engines
(for your own sanity)
When building out your apps there invariable comes a point when you’re either dealing with lots of boilerplate1 or chasing down bugs related to literal content2.
At this point you’re ready for some form of code generation tool — so here’s some thoughts on picking the right templating engine or at a higher level code generation tool.
Rules
Like all rules apply them sensible, but having said that when a project hits this point these are rules I try to follow:
- Choose the right template engine
- Don’t fight the tools
- Don’t produce output that a linter or code formatter will change
- Don’t annoy your other tools
- Don’t annoy other developers
Choose the right template engine
Not all template engines are the same, broadly speaking they fall into two overall categories:
- Logic-less templating engines
- Semantic Logic templating engines
Logic-less templates are fast, easy to use and rarely have any hidden problems based on mistakes in your logic, of course though they’re also best for simpler use cases. The long time leader in this field is the Mustache engine which has implementations in most of the major languages.
If you need to create and use “helper” functions, have template inheritance, make decisions in your template based on object contents or dynamically include complex sub-templates you’ll be leaning towards tools like Twig, Jinja 2, Handlebars.js.
Of course like all development platforms there are a range of template engines for Swift and Objective-c. Prominent amongst these are Stencil3, Mustache4, Leaf5 and highly specific tools like GYB.
Depending on your needs you may not need a template engine at all but a higher level tool like SwiftGen6 or Sourcery.
If all you want to do is generate type safe access to literal content (strings, colours, media assets, etc) then tools like SwiftGen and Sourcery probably the path you want.
Don’t fight the tools
Each templating systems has implementation details that are focused on it’s intended domain of use. Pick a templating engine that supports your development environment — if you’re generating HTML for PHP based system it makes little sense to install Node.js
simply so to use Twing
7
Pro-tip: Every template engine has a different set of quirks when it comes to the whitespace in a template file, learn the quirks to produce the correct output.
Almost all good template engines support modularisation of common operations — use them, it makes your maintenance easier.
Don’t produce output that a linter or code formatter will change 8
If you find yourself reaching for a linter or formatter to “clean-up” the output from your templates you’re doing something wrong. This is the templating equivalent of a “code smell” — fix your template not your output.
If you fix your template you don’t have to deal with changes to the linter or formatting tools causing unexpected outcomes.
A caveat: do use your linter or formatter to validate the generated output against your current requirements, and throw a build error if it doesn’t meet those requirements9
Don’t annoy your other tools
File Extensions
Your templates will output HTML, Swift or some other file type, but generally they are not valid HTML, Swift, etc files. So it’s important to make sure the suffix of your template reflects the template engines extension. There’s nothing worse than having your compiler/linter/formatter choke on your template file.
Sub-Extensions
Another good habit to get into is to include the extension of the generated output. This is best explained by example, so if we’re using Jinja2 the templates extension will be .j2
and if the template in question is producing HTML as it’s output we prefix the templates extension with .html
and finally the we can add the file name. So for the index of a website directory our template would be called index.html.j2
and it’s output will be index.html
Where this becomes useful is when you want to generate other file types with the same name — like CSS and Javascript for the same page.10
index.html.j2 ➡︎ index.html
index.js.j2 ➡︎ index.js
index.css.j2 ➡︎ index.css
Similarly if your using SwiftGen
or Sourcery
, which both use StencilKit
then your templates should be named similarly to this:
CloudApi.swift.stencil → Cloud/Generated/User.swift
→ Cloud/Generated/Devices/Device.swift
→ Cloud/Generated/Devices/Config.swift
→ Cloud/Generated/Devices/Profile.swift
→ Cloud/Generated/Organisations.swift
(A single template of course can generate multiple files.)
Logical Files
A common mistake that users new to templated code generation make is put all the output in a single file. Don’t do this.
If you think about it this is bad for a variety of reasons, here’s just a few:
- You can kill your IDE when it tries to index and parse a single file with thousands of lines of code (including any interaction with the compiler)
- A tiny API change will regenerate the entire file making diffing in git problematic at best (individual type/class files may result is just a few changes that are easier for everyone).
- Do you really want to debug 30k lines of generated code? (That’s not an exaggeration — I’ve actually seen larger.)
Don’t annoy other developers
Single Instance files
For singular generated files (e.g. those generated to support a single asset catalogue) the generated file name should make it clear that the contents of the files are generated so that they don't have to view it's contents to understand this fact.
Users/UserAvatars-generated.swift
Users/UserAvatars.xcassets
Users/UserProfile.swift
Grouping
If the API you’re generating code from has a hierarchy group the output appropriately in a directory structure (see Stencil example above).
If you have generated and hand-crafted code in the same project consider placing all generated code within a /Generated/
11 sub-directory. For example extending the Stencil example the Cloud
directory could look like this:
Cloud/Generated/User.swift
Cloud/Generated/Devices/Device.swift
Cloud/Generated/Devices/Config.swift
Cloud/Generated/Devices/Profile.swift
Cloud/Generated/Organisations.swift
Cloud/Networking/Endpoint.swift
Cloug/Networking/Security.swift
File Headers
Make it clear in all code-generated file that:
- It is a product of code generation
- The tools used to generate it
- For large projects with multiple templates indicate the template that should be edited to update the file.
Minimal SwiftGen Default Header
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
More detailed Jinja2 Header
/* WARNING: This is a generated file, do not edit.
To make changes, edit web/templates/api/login.php.j2 and then run ./generate.py
*/
Photo by Antonio Batinić from Pexels
-
Think of API definitions from tools like Swagger that you want to implement in Swift. ↩
-
Literal content are elements that are accessed by a string literal (e.g. image or other media accessed by name) or created from literal values (colour expressed in
hex
or RGB values) ↩ -
Born out of the IBM Kitura Serverside Swift project is has a battle proven implementation. ↩
-
Technically GRMustache but… yeah ☺️ ↩
-
Not that great for code-gen but a great Swift platform tool for generating complex content. ↩
-
Great for generating type safe access to
- Assets Catalogs
- Colors
- Core Data
- Fonts
- Interface Builder files
- JSON and YAML files
- Plists
- Localizable strings
-
Literally a re-implementation of the far better PHP Twig template engine — and yes people do this 😂 ↩
-
Linters and formatters are not the same as minimisers or obfuscators which are often a functional requirement and completely acceptable post generation. ↩
-
In other words use the problems found by these tools to fix your templates. ↩
-
Someone reading this will be saying yeah but I’m never going to generate any other type of file… and that’s fine… but someone else in your team or even your future self will and then… 🙁 ↩
-
Or similar name. ↩