Generating Swift Types with Sourcery

Sourcery, the Swift code generator With the announcement of Xcode 9.3 beta we finally get to see synthesized == implementations for Equatable Swift types. Our app was once full of implementations like this: struct Model { var data: String} extension Model: Equatable {}func == (lhs: Model, rhs: Model) -> Bool { return lhs.data == rhs.data} Now […]

Sourcery, the Swift code generator

With the announcement of Xcode 9.3 beta we finally get to see synthesized == implementations for Equatable Swift types. Our app was once full of implementations like this:

struct Model {
var data: String
}
extension Model: Equatable {}
func == (lhs: Model, rhs: Model) -> Bool {
return lhs.data == rhs.data
}

Now all we will need is:

struct Model: Equatable {
var data: String
}

As long as all our stored properties are Equatable we’ll never need to maintain a == implementation ourselves. That’s good news because as we added properties to a model nothing forced us to also compare them in == which could lead to surprising results.

Great news once we adopt Xcode 9.3, but we’ve had a Swift app for years now and couldn’t wait. Today, using Xcode 9.2, our app contains 186 Equatable types we don’t need to hand craft == functions for.

Reading through our app’s source you’ll run across lots of types that look like this:

struct ViewModel: AutoEquatable {
var title: String
var details: String
}

What’s AutoEquatable? If we jump to it’s definition we’ll find that’s it’s just an empty protocol: protocol AutoEquatable {}. Looking at other uses of the model we might eventually find an entry in AutoEquatable.generated.swift, along side dozens of other == functions. It looks like this:

// MARK: - ViewModel AutoEquatable
extension ViewModel: Equatable {}
internal func == (lhs: ViewModel, rhs: ViewModel) -> Bool {
guard lhs.title == rhs.title else { return false }
guard lhs.details == rhs.details else { return false }
return true
}

So what’s going on here? When we introduce or modify an AutoEquatable type we run a script which invokes Sourcery to regenerate all of our == implementations.

#!/usr/bin/env bash
set -e
BIN_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_DIR="$( cd "$BIN_DIR/.." && pwd)"
“$PROJECT_DIR/Pods/Sourcery/bin/sourcery” 
— sources “$PROJECT_DIR/app_name”
— templates “$PROJECT_DIR/generator_templates/sourcery”
— output “$PROJECT_DIR/app_name/generators/generated/sourcery”

So will Xcode 9.3 be the end of our use of Sourcery? We might be done with AutoEquatable but it won’t be the end of Sourcery for us.

Take a look at some of the other bundled templates that ship with Sourcery. Swift types don’t allow us to inject test doubles (I still miss OCMock) but Sourcery can automatically generate mock implementations of any type we care to annotate with an AutoMockable protocol.

protocol URLOpener: AutoMockable {
@discardableResult
func openURL(_ url: URL) -> Bool
}
final class MockURLOpener: URLOpener {
  // MARK: - openURL
  var openURLCalls = [URL]()
var openURLReturnValue: Bool!
  func openURL(_ url: URL) -> Bool {
openURLCalls.append(url)
return openURLReturnValue
}
}
// ...
expect(mockURLOpener.openURLCalls.count).to(equal(1))
expect(mockURLOpener.openURLCalls.first).to(equal(url))

Adding in custom templates we’ve eliminated even more boilerplate and manual code maintenance. Our app uses a unidirectional data flow pattern powered by ReSwift and many of our actions and reducers are now generated by Sourcery.

After 6 months of using Sourcery we have a couple of lessons learned I would apply to my next app.

Version all your dependencies, dev tools included.

Installing Sourcery (and our other binary dependencies like SwiftLint) via CocoaPods and commiting the pods has worked great. Our CI machines have no idea how to install our dependencies and don’t need to. All we need to do is checkout a specific git commit and compile. We’ve never had to worry about changes to our templates suddenly appearing during a CI build, old versions of dependencies becoming unavailable, or mismatched versions between development machines.

Build times are already long enough.

With our current templates running against 1,384 types takes 3.8 seconds on my development laptop. I could afford to run that as part of a Run Script build phase but don’t really need the extra delay. I’ve found that our annotated types change infrequently enough that a script checked into the project and run manually works well for me. That said we did have outdated generated files make it into our repository until we added a step to our CI build that reruns Sourcery and fails the build if the files change.

#!/usr/bin/env bash
set -e
BIN_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_DIR="$( cd "$BIN_DIR/.." && pwd)"
$BIN_DIR/generator_all.sh
IS_DIRTY="$(git status "$PROJECT_DIR/app_name/generators/generated/" | grep modified || true)"
if [ "$IS_DIRTY" ]; then
echo "$IS_DIRTY"
echo "!!! Generated files out of sync with git. Run bin/generator_all.sh and commit the result. !!!"
exit 1
fi

Protocols, protocols everywhere.

AutoMockable really encourages protocol oriented development. I find this to be a bonus but I’m glad we started with that sort of pattern to allow us to manually build test doubles. If we weren’t already injecting dependecies as protocols swapping in mocks during testing would have required alot more work.

Annotations add information for your templates.

Sometimes there’s just not enough information in the source code to generate the templates I want. In those cases Sourcery’s annotations offer a great way to customize the template output at the cost of a little more typing. I use them to specify a common base name when we generate several types from a single base type. For example:

//sourcery: componentName = "BrowseSubcategory"
struct BrowseSubcategoryComponentState: ComponentState {

Generates BrowseSubcategoryComponentID, BrowseSubcategoryComponentAction, RegisterBrowseSubcategoryComponent, and DeregisterBrowseSubcategoryComponent types.

What are we missing? Are you using Sourcery or other templates in your apps and what problems have they solved for you?

If this sounds like something you wish you were using, my team at Good Eggs is growing. We’re delivering incredible locally produced food to Bay Area homes. If you are inspired by our mission is to grow and sustain local food systems worldwide, find out how you can help.


Generating Swift Types with Sourcery was originally published in Good Eggs Product Team on Medium, where people are continuing the conversation by highlighting and responding to this story.

Source: Good Eggs