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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Sourcery Tutorial: Generating Swift code for iOS
30 mins
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.
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.
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.
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!
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.
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.