Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

Sourcery Tutorial: Generating Swift code for iOS

$
0
0

Sourcery Tutorial: Generating Swift code for iOS

If you’re like most developers, writing the same thing over and over can get pretty boring. You might find yourself doing the same thing five different ways purely out of a need for variety.

While it’s not a bad idea to find better solutions to the same problem, it is a bad idea to have these solutions littered across your projects. The worst thing you can do to your project is muddle the underlying architecture principles with various ideas.

So where does that leave you? Writing boring boilerplate code? Maybe you can think up some super generic magically dynamic framework to do the work for you. But three months from now, you’ll try to untangle your undocumented magic code and have no idea what is going on.

So, what if there was a way to have someone write the tedious boilerplate code for you, do it to spec with 100% accuracy — and work for free?

Sourcery is an open source tool that generates Swift code from predefined templates. But it’s not simply a dumb macro that spits out source files based on a set of rules. It’s a smart tool that scans your source code and uses that information in your templates, which means you can create very detailed solutions.

This Sourcery tutorial is aimed at intermediate Swift developers. Since you will be writing templates to generate Swift code, you should have a solid understanding of the Swift language, Xcode, and some experience using the command line via Terminal.

Getting Started

First and foremost, install Sourcery. There are a number of ways to do this, and they’re described under the Installing section of the project’s Readme on GitHub.

I opted to install the binary from the project’s Releases page.

Once the binary is downloaded and extracted you can simply copy the contents of the bin directory to /usr/local/bin. Given that /usr/local/bin is on your $PATH you will be able to run Sourcery by typing sourcery from any directory in Terminal.

Once you have Sourcery installed run sourcery --help from Terminal to verify it is working, you should see something like the following:

sourcery --help result

The next step is to download the starter project. Unzip the downloaded file and open Brew Guide.xcodeproj.

This Sourcery tutorial has an example app that lets you browse data from the Brew Guide at brewerydb.com. You’ll require an API key to access the data, but it’s a quick, free, and painless process.

Note: This Sourcery tutorial will get you going writing templates, and generating source code. The sample app is not the main focus of the tutorial; rather, it’s an example to show how Sourcery can be applied to a project. At the end of the Sourcery tutorial you will be sent off on your own to do further studying of provided Sourcery templates and the sample app’s source code.

BreweryDB API Key

To register for an API key, head over to the Developer section of the site. Click Create An Account and fill out the registration form. You will need to agree to their terms of service. After agreeing, you’ll be taken to your account screen where you can switch to the My Apps section and click Register A New App.

Registering an app on BreweryDB

Registering a new app requires you to fill in an App Name, Description, Website and Platform. Enter anything you want for these fields, or use the following:

Registering an app on BreweryDB

Once you complete the registration form, you’ll be taken back to your account screen which will list the newly registered app along with its API Key.

BreweryDB API key location

Copy the API Key to your clipboard and go back to Xcode. Open StylesTableViewController.swift. At the top you will find an instance variable named apiManager initialized inline. Replace YOUR_API_KEY with the key in your clipboard.

At this point you’re all set to continue with the Sourcery tutorial. Running the app won’t yield any interesting results, as you have yet to to the legwork to retrieve data from the BreweryDB API!

Sourcery 101

Before diving in, it’s a good idea to get a handle on the basics of Sourcery. The Sourcery templating system is by and far the most important aspect to understand when using Sourcery.

There are three types of template languages that you can choose from: Stencil, Swift, and JavaScript. For this tutorial, the focus will be on Stencil. While it may seem obvious to go with Swift, I personally felt the Stencil templating language was easier to write and better documented. It simply feels more mature for this task.

To get your feet wet, you’ll write a template that generates the requirements for the Swift Equatable protocol.

Open the playground named AutoEquatable.playground under the appetizer directory in the downloaded starter project. With the playground opened, open the Navigator pane if it is not already by pressing ⌘+0 or going to View, Navigators, Show Navigator. Once the Navigator is visible, twist open the Sources directory and open Person.swift.

Contents of Person.swift

This file defines a basic struct for a Person type. Your goal is to add Equatable conformance using Sourcery.

The first step is to create a new protocol that will be used to effectively annotate the Person type so Sourcery knows which types to make Equatable. Define a new protocol below import Foundation:

protocol AutoEquatable { }

This is a requirement-free protocol. Now mark Person as a conformer of the AutoEquatable protocol:

public struct Person: AutoEquatable {
    public let name: String
    public let age: Int
    public let gender: Gender

    public init(name: String, age: Int, gender: Gender) {
        self.name = name
        self.age = age
        self.gender = gender
    }
}

If you’re confused, don’t worry; the reasoning is coming.

Now, using your favorite text editor create an empty text file named AutoEquatable.stencil in the same directory as AutoEquatable.playground. Add the following Stencil template code to the file and save it.

{% for type in types.implementing.AutoEquatable %}
// hello, sourcery!
{% endfor %}

This probably looks foreign if you’ve never used Stencil. This tells Sourcery to search all of the source files you’ve provided for types that implement AutoEquatable. For each type found, it will print // hello, sourcery!. The important thing to note is that anything between {% %} is interpreted as Stencil code; everything in the brackets needs to “compile” and follow the Stencil syntax requirements. Anything outside of the brackets, however, is printed straight to the generated file.

Using Terminal, change to the same directory as the AutoEquatable.playground file and run the following command:

sourcery --sources AutoEquatable.playground/Sources \
--output AutoEquatable.playground/Sources \
--templates . \
--watch

There are a few flags here to tell Sourcery what to do:

  • --sources: Scan for .swift files under AutoEquatable.playground/Sources.
  • --output: Put generated files under AutoEquatable.playground/Sources.
  • --templates: Find all template files in the current directory.
  • --watch: Continue running and generating files anytime a template or source file changes. You must terminate the process when finished using CTRL+C.

Go back to the Xcode Playground, and you should see a new file under Sources named AutoEquatable.generated.swift. Open it up.

Contents of AutoEquatable.generated.swift

Great work! You’ve generated your first file. Now switch back to your text editor with AutoEquatable.stencil open.

If you can, arrange your windows side-by-side so that you’re viewing both the stencil file and AutoEquatable.generated.swift in Xcode Playgrounds. If not, be ready to switch back and forth between the two. As you make changes in the stencil template, you will see updates in the generated file. This is incredibly helpful to catch errors quickly.

Update the stencil template with the following:

{% for type in types.implementing.AutoEquatable %}
extension {{ type.name }}: Equatable {

}
{% endfor %}

This tells Sourcery to create an extension for each type implementing AutoEquatable, and mark the type as a conformer of Equatable. What you may notice is that this looks like Swift — and it is! You’re simply writing Swift code inline with the Stencil templating language.

The non-Swift part is {{ type.name }}, which prints out the value of the Stencil property between the brackets; in this case, the type’s name. Save the stencil template and go back to AutoEquatable.generated.swift.

Contents of AutoEquatable.generated.swift

There’s a compilation error, but… wow. Are you feeling powerful yet? Isn’t it awesome how you didn’t tell Sourcery anything about Person, but it was able to pick it up from the AutoEquatable protocol and print its name right there in the generated file? Like magic… err, sorcery!

Get your wand and wizard hat ready, because you’re in for a treat.

Go back to AutoEquatable.stencil and add the following to your template within the brackets of the extension.

  public static func ==(lhs: {{ type.name }}, rhs: {{ type.name }}) -> Bool {
    {% for var in type.variables %}
    guard lhs.{{ var.name }} == rhs.{{ var.name }} else { return false }
    {% endfor %}

    return true
  }

Aside from a few places, this is mostly Swift code. {{ type.name }} is used again in a number of places, and another for-loop is defined to iterate over the type’s variables. The syntax of Stencil for-loops is very similar to Swift for-loops.

Jump back to the generated file to see what happened.

Contents of AutoEquatable.generated.swift

Whoa — all that tedious boilerplate code was magically written for you!

Go back to the main playground page by clicking on AutoEquatable in the navigator, and uncomment the equality tests to verify your generated code is working.

If you followed along the Sourcery tutorial correctly, you should see equality tests passing and failing correctly.

Nice work!

Sample App Architecture

Before you start generating source code for an app, you should have a clear picture of what your architecture is, or which design patterns you want to follow. It doesn’t make sense to script something until it’s clear what is being repeated, right? This section is all theory, so don’t worry about adding or editing any files in your project just yet.

The sample app’s networking architecture has already been thought out. But you should understand what it is so you’re clear on what the templates you write are for. Like all networking stacks, there will be some component that takes a request, sends it and handles the response from a server. That component will be called APIManager: a class with a method that takes an APIRequest instance as well as a completion closure that is executed when the network request finishes.

Its signature is as follows:

public func send<R: APIRequest>(_ request: R, completion: @escaping (APIResult<R.Response>) -> Void)

The APIManager class comes fully implemented for the purpose of this tutorial using the power of Swift’s protocol-oriented-programming. While you may want to see the inner workings of send(_:completion:), understanding that is not the purpose of the tutorial. You will however need to be familiar with the following protocols.

Most of these are very basic, but stay tuned.

JSONDecodable

Allows a type to be initialized with a [String: Any] typed dictionary.

public protocol JSONDecodable {
  init?(json: [String: Any])
}

JSONDecodableAPIModel

A more explicitly named type that conforms to JSONDecodable, there are no additional requirements for this type.

public protocol JSONDecodableAPIModel: JSONDecodable {}

APIResponse

A type that represents a response from the API that can be initialized with a JSON dictionary, as it is JSONDecodable, has an associatedtype named Model that conforms to JSONDecodableAPIModel, and a status string to hold the status returned from the API.

The most interesting part here is the associatedtype, Model; you’ll see why soon.

public protocol APIResponse: JSONDecodable {
  associatedtype Model: JSONDecodableAPIModel
  var status: String { get }
}

RESTful/RESTlike/JSON APIs typically return a body of JSON with some metadata about the request and response, as well as a nested data model. Sometimes it is a single entity, but often it’s an array of entities. To represent both scenarios there are two more protocols conforming to APIResponse.

APIEntityResponse

An API response for a single record, accessible through a data property. Notice how the type is Model. This is referencing the associatedtype defined in APIResponse.

public protocol APIEntityResponse: APIResponse {
  var data: Model { get }
}

APICollectionResponse

Much like APIEntityResponse this type has a data property but it is an array of Model instances. This response type is used when the API returns more than one record.

public protocol APICollectionResponse: APIResponse {
  var data: [Model] { get }
}

So there are requirements for initialization with JSON as well as what a response type looks like. What about requests?

APIRequest

This type defines a request to be sent to the API for an associated APIResponse type. If you’re comfortable with protocols or generic programming, you may be starting to see the picture here.

This type also requires httpMethod, path, and any queryItems necessary to make the call.

public protocol APIRequest {
  associatedtype Response: APIResponse
  var httpMethod: String { get }
  var path: String { get }
  var queryItems: [URLQueryItem] { get }
}

Whew, that’s a lot to digest. So how does it all fit together? Take a look back at the APIManager method for sending a request.

public func send<R: APIRequest>(request: R, completion: @escaping (APIResult<R.Response>) -> Void)

Notice that the completion handler takes a parameter with the type APIResult<R.Response>. The APIResult type has not yet been introduced, but you can see that it is a generic type. R.Response is listed in the generic parameter clause, where R is an APIRequest and Response is the request’s associated APIResponse.

Now, look at the definition of APIResult.

public enum APIResult<Response> {
  case success(Response)
  case failure(Error)
}

It’s a simple enum that’s either success with an associated value or failure with an associated Error. In the case of the send method, the associated value for a successful result is the associated response type for the request, Which if you recall, has to be of the type APIResponse.

The first view in the Brew Guide app is a list of beer styles. To get those styles, you make a request to the /styles endpoint which returns an array of Style models. Since an APIRequest definition requires an APIResponse, you will define that first.

Open GetStylesResponse.swift in the BreweryDBKit/Responses folder, and add the following protocol conformance:

public struct GetStylesResponse: APICollectionResponse {
  public typealias Model = Style

  public let status: String
  public let data: [Style]

  public init?(json: [String: Any]) {
    guard let status = json["status"] as? String else { return nil }
    self.status = status

    if let dataArray = json["data"] as? [[String: Any]] {
        self.data = dataArray.flatMap { return Style(json: $0) }
    } else {
        self.data = []
    }

  }
}

The response conforms to APICollectionResponse, defines its associatedtype Model as Style, and initializes with the JSON response from the server.

Now, you can define a request object by navigating to GetStylesRequest.swift and adding the following:

public struct GetStylesRequest: APIRequest {
  public typealias Response = GetStylesResponse

  public let httpMethod = "GET"
  public let path = "styles"
  public let queryItems: [URLQueryItem] = []

  public init() {
  }
}

Here you satisfy the requirements for APIRequest and define the associatedtype Response as GetStylesResponse. The beauty of all of this is that now when you send a request, you get back the model you expect with no extra work on your part. All of the JSON deserialization is done for you by the APIManager‘s send method using the methods and properties defined through this series of protocols. Here’s what it looks like in use.

let stylesRequest = GetStylesRequest()
apiManager.send(request: stylesRequest) { (result) in
  switch result {
  case .failure(let error):
    print("Failed to get styles: \(error.localizedDescription)")
  case .success(let response):
    let styles = response.data // <- This type is [Style]
  }
}

Now you're just left to writing these really boring APIRequest and APIResponse definitions. Sounds like a job for Sourcery!

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.

Where to Go From Here?

You can download the final project here.

This Sourcery tutorial gave you a taste for the efficiency and accuracy that Sourcery can bring to your projects. If you enjoyed it, think about the areas in your projects that feel redundant – where you're on cruise control banging implementations.

Think about how you might incorporate a template to generate code for you. Think about how you can re-architect areas of your app to be boring and simple enough to be generated. This takes practice, but it is a good practice. Your future self or teammates will appreciate the upfront thought put into the project.

For resources on how to take full advantage of Sourcery check out the following:

  • Sourcery GitHub: Sourcery's repository, to find new releases and documentation.
  • Sourcery Documentation: The full documentation for Sourcery.
  • Stencil Website: The official website for the Stencil templating language. You'll find more documentation on how to write Stencil templates here.

If you have any comments or questions about Sourcery, join the discussion below!

The post Sourcery Tutorial: Generating Swift code for iOS appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles