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
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!