How to supercharge Swift enum-based states with Sourcery

Alexey Demedeckiy
6 min readJan 11, 2018

--

I really love enums with associated values in Swift and it’s my main tool for designing state. Why are enums so much more powerful and beautiful option? Mostly because they allow me to keep strong invariants about the data in my type system.

For example, if I have a screen which will display some data loaded from the server, I can represent it with next model:

struct MyScreenModel {
let isLoaded: Bool
let isEmpty: Bool
let data: SomeInfo?
let error: Error?
}

Then I can write unit tests to check correctness of this model and add documentation with expected combination flags and optionals.

On the other hand, I can represent it with an enum:

enum MyScreenModel {
case empty
case loading
case data(SomeInfo)
case failed(Error)
}

Now my type reflects all invariants that I have on my data. This means that compiler will be able to check it and I can remove unit tests responsible for verification of these invariants.

Pros are clear and loud, what about cons? What will we pay for this kind of guarantees? Let’s compare how easy it is to work with each of these types:

func viewWillLayoutSubviews() {
self.loadingIndicator.isHidden = self.model.isLoading == false
...
}

compared to our previous version:

func viewWillLayoutSubviews() {
self.loadingIndicator.isHidden = {
guard case .loading = self.model else { return true }
return false
}()
...
}

Obviously, something bad is happening. We lose ability to refer to specific parts of our model, and now we are forced to double check what is exactly stored in this model.

Editing makes the situation even worse. Let’s declare SomeInfo struct as mutable and look how it will affect our code:

struct SomeInfo {
var name: String
}

In first, naive, unsafe way:

self.model.info?.name = "New name"

And in safe, enum way:

self.model = {
guard var case .info(info) = self.model else { return self.model }
info.name = "New name"
return .info(info)
}()

Obviously, this is wrong. Can we achieve usability of struct-based approach and safety of enum-based approach?

Prism to the rescue

What is prism? Generally speaking, prism is an object capable of decomposing some beam into details, making details visible and obvious.

What is prism in terms of our problem domain?

If our enum version of data structure is a solid beam, then our struct is a “spectre”. So prism is a way to turn enum into struct. There are several ways to achieve it, and I will showcase the extension based approach:

extension MyScreenModel {
var isLoading: Bool {
guard case .loading = self else { return false }
return true
}

var isEmpty: Bool {
guard case .empty = self else { return false }
return true
}
var data: SomeInfo? {
guard let case .data(someInfo) = self else { return nil }
return someInfo
}
var error: Error? {
guard let case .error(error) = self else { return nil }
return error
}
}

And now we can go back to short and concise syntax of struct based variant and have compiler guarantees from enum based option. Win-Win!

The only downside: writing these extensions is boring.

Adding some Sourcery

Sourcery is the standalone tool that allows you to wire up some information about your code with template language called Stencil. In other words, it is cool code generation driven approach by your own code. If you are not familiar with the syntax of Stencil, don’t worry. I also referred to my intuition and google rather than some knowledge and understanding.

So what do I want to get? I want to have an extension for all enum. Let’s make it:

{% for enum in types.enums where enum.cases.all.count > 0 and not enum.accessLevel == 'private' %}  ...
{% endfor %}

This code is so nice that I don’t need to write any comments. It is basically self-explainable. And what do we want for each enum? Extension!

{{ enum.accessLevel }} extension {{ enum.name }} {
...
}

What’s next? For each case we need to generate a property.

{% for case in enum.cases %}
...
{% endfor %}

What do we want to generate? If our case contains some associated value we want optional accessor, otherwise — boolean.

{% if case.hasAssociatedValue %}
{% call associatedValueVar case %}
{% else %}
{% call simpleVar case %}
{% endif %}

We found some cool function like calling syntax!
Let’s look on simple case implementation:

{% macro simpleVar case %}
public var is{{ case.name|upperFirst }}: Bool {
get {
guard case .{{ case.name }} = self else { return false }
return true
}
set {
guard newValue else {
fatalError("Setting false value forbidden")
}
self = .{{ case.name }}
}
}
{% endmacro %}

For getter we just check self and return true or false. Setter allow us to switch enum like this:

var model = MyScreenModel.emptymodel.isLoading = true
// is the same as
model = .loading

Setting false doesn’t have any sense, so I must add runtime check to catch it.
What about cases with associated types?

{% macro associatedValueVar case %}
public var {{case.name}}: {% call caseType case %} {
get {
guard case let .{{ case.name }}({{ case.name }}) = self else {
return nil
}
return {{case.name}}
}
set {
guard let newValue = newValue else {
fatalError("Setting nil value forbidden")
}
self = .{{ case.name }}({% call caseSet case %})
}
}
{% endmacro %}

Pretty simple, I would say. It is tricky at first, but 3 min look into it and it becomes really simple. Note that caseType and caseSet macro are out of scope here, but they mostly are here for normalizing access to type names.

The full text of template you can easily find on public production code base.

Connecting Sorcery to Xcode

So we have our template. How to actually generate code with it?

There are plenty of ways to do it in Sourcery README file, but I want to show you the way that we found for ourself.

Step 1. Add Xcode build rule to support *.stencilfiles.

Go to Project -> Build rules -> “+”
And make it look like this:

set -eif ! which sourcery > /dev/null; then
echo "error: Sourcery is missing. Make brew install sourcery."
exit 1
fi
templates=$1
output=$2
sourcery --sources sources --templates "${templates}" --output "${output}"

Where sources/stencil.sh is a simple wrapper around sourcery tool itself.

As a result of this rule, every stencil file will be processed to some swift file, which later will be compiled. The generated file will be stored into derived sources dir and never will appear in git or code review. It will be updated in every build and always will be relevant.

In other words — it just works.

Step 2. Move your stencil templates to compile phase

Step 3. Build your project.

Sometimes something inside Xcode dies, and this setup will not bring you updated version. In case of such problems — just clean derived data, as usual.

I hope my brief guide will be helpful for some of you :) If you want to learn more and look at real project that use this technique, you can examine our production code hosted here.

Thanks for reading.

--

--