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 2 of 4 of this article. Click here to view the first page.

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!

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.