Sourcery Tutorial: Generating Swift code for iOS

What if someone could write boilerplate Swift code for you? In this Sourcery tutorial, you’ll learn how to make Sourcery do just that! By Chris Wagner.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Advanced Template Writing

Now you'll work on a template to begin supporting the architecture described above.

Before requests are made and responses are parsed, you need to create the models to support them. For Brew Guide, those models need to conform to JSONDecodableAPIModel. A good practice when using Sourcery is to create empty protocols for annotating your types that Sourcery should generate code for. You did this above with the AutoEquatable protocol.

Open Brew Guide.xcodeproject from the starter project and then Protocols.swift located under the BreweryDBKit folder.

Add a new protocol right above JSONDecodableAPIModel.

public protocol AutoJSONDecodableAPIModel {}

In your text editor, create a new file named AutoJSONDecodableAPIModel.stencil and save it under the SourceryTemplates directory. This template will be used to generate the implementations of init?(json: [String: Any]) for the JSONDecodable protocol for each model that conforms to AutoJSONDecodableAPIModel.

Thankfully, JSON deserialization is a lot better in Swift 4 than it was in Swift 3. However, parsing [String: Any] dictionaries can still be tedious, so it's an excellent candidate for automation. You will start with the Style model.

Open Style.swift and add the following:

public struct Style: AutoJSONDecodableAPIModel {
  public let id: Int
  public let categoryId: Int
  public let name: String
  public let shortName: String
  public let description: String
}

The type is marked as AutoJSONDecodableAPIModel and a number of properties are added. Each property name is conveniently the same as the keys returned in the JSON response. Your goal is to write a Sourcery template to generate the following:

extension Style: JSONDecodableAPIModel {
  public init?(json: [String: Any]) {
    guard let id = json["id"] as? Int else { return nil }
    self.id = id
    guard let categoryId = json["categoryId"] as? Int else { return nil }
    self.categoryId = categoryId
    guard let name = json["name"] as? String else { return nil }
    self.name = name
    guard let shortName = json["shortName"] as? String else { return nil }
    self.shortName = shortName
    guard let description = json["description"] as? String else { return nil }
    self.description = description
  }
}

In AutoJSONDecodableAPIModel.stencil, add the following:

{% for type in types.implementing.AutoJSONDecodableAPIModel %}

// TODO: Implement

{% endfor %}

Now, from Terminal start Sourcery with the --watch flag so you can watch the results as you make changes. From the root level of the starter project directory, run the following:

sourcery --sources BreweryDBKit \
--templates SourceryTemplates \
--output BreweryDBKit/Generated \
--watch

This will start Sourcery and immediately generate an output file of AutoJSONDecodableAPIModel.generated.swift. You need to add this file to your Xcode project so it is included in the builds.

Control-click on the Generated folder under BreweryDBKit in Xcode and choose Add Files to "Brew Guide" .... Select AutoJSONDecodableAPIModel.generated.swift.

Adding a new file to an Xcode group

Open the generated file in Xcode to see and watch the results of your template. Right now it'll only have the standard Sourcery header and the TODO comment from your template.

With the the AutoJSONDecodableAPIModel.generated.swift file opened, swing open Utilities and under the File inspector, make sure Target Membership is checked for BreweryDBKit.

Checking Target Membership for a file

Go back to AutoJSONDecodableAPIModel.stencil and update the for-loop body with the following:

extension {{ type.name }}: JSONDecodableAPIModel {
  public init?(json: [String: Any]) {
  }
}

This creates an extension for every type implementing AutoJSONDecodableAPIModel and adds the required init?(json:) method with an empty implementation. What you want to do next is iterate over each of the type's variables and extract the value from the JSON dictionary.

Update the body of init?(json:) with:

{% for variable in type.variables %}
guard let {{variable.name}} = json["{{variable.name}}"] as? {{variable.typeName}} else { return nil }
self.{{variable.name}} = {{variable.name}}
{% endfor %}

This starts another for-loop for each variable defined on the type, generates a guard statement, accesses a key in the json dictionary matching the variable name, and attempts to cast the acessed value to the variables type. Otherwise, it returns nil. If the value is successfully extracted, it’s set on the instance. Save the template and check the generated code.

Generated code for the Sourcery tutorial

Success! You've completed your goal. Now watch how easy it is to do the same for another model. Open Brewery.swift and update it with the following definition:

public struct Brewery: AutoJSONDecodableAPIModel {
  public let id: String
  public let name: String
  public let description: String
  public let website: String
  public let established: String
}

Jump back to AutoJSONDecodableAPIModel.generated.swift, and BAM!

Sourcery also generated the code for a Brewery

Just like that, you have deserialization code for the Brewery model as well.

These are pretty simple models with basic properties; there are no optionals, no collections, and no custom types. In the real world it's not always so simple. And for the Beer model, things get a bit more complicated.

Open Beer.swift and update it with the following:

public struct Beer: AutoJSONDecodableAPIModel {
  
  public let id: String
  public let name: String
  public let description: String
  public let abv: String?
  public let ibu: String?
  
  public let style: Style?
  public let labels: Labels?
  public let breweries: [Brewery]?
  
  public struct Labels: AutoJSONDecodableAPIModel {
    public let icon: String
    public let medium: String
    public let large: String
    
    // sourcery:begin: ignore
    public var iconUrl: URL?   { return URL(string: icon) }
    public var mediumUrl: URL? { return URL(string: medium) }
    public var largeUrl: URL?  { return URL(string: large) }
    // sourcery:end
  }
}

There's a lot going on here, as some properties are optional. There is an array of Brewery models, there's a nested type, and even some strange comments.

Unfortunately, in this tutorial you won't be able to walk through all of the details of the template updates required to make this work, but the template is available for your use. There are also templates available for assisting with APIRequest and APIResponse implementations.

At this point, the starter project won’t compile because the template isn’t generating the right code for the nested Labels type. You are, however, well on your way to becoming a Sorcerer! Uh, err... Sourcery master!

You can challenge yourself to write the templates yourself, or jump into the final project and review them. You can find the full documentation for Sourcery here.

To see the finalized templates, download the final project and look in the SourceryTemplates directory. Each template is documented using Stencil's comment tag which looks like {# This is a comment #}.

The final project download includes the generated Sourcery files, but you can run the sourcery command yourself, too. Doing so is as simple as running sourcery as there is a .sourcery.yml file at the root level that configures the sources, templates, and output locations. This is a nice thing to add to your projects so that you don’t need to remember the long-form command with all of the flags. You will find the final, generated files under the BreweryDBKit/Generated group in Xcode.

Add your API Key to StylesTableViewController.swift like you did for the starter project and build and run.

Sourcery tutorial final app

The app will load a list of styles. You can select a style, and then select a beer to view its details. This project is here to use as a guide and example for utilizing Sourcery in your projects.

Chris Wagner

Contributors

Chris Wagner

Author

Marin Bencevic

Tech Editor

Chris Belanger

Editor

Essan Parto

Final Pass Editor

Andy Obusek

Team Lead

Over 300 content creators. Join our team.