Advanced Generics and protocols in Swift.

James Rochabrun
8 min readDec 19, 2018
https://swapi.co/

Well, here we go again, one more time I will like to talk about generics and protocols in Swift 4.2.

A few days ago I gave me a small challenge, the goal? implement a client API for SWAPI the “Star Wars API”, made by Paul Hallett, avoiding code repetition as much possible but still keeping things making sense.

On this tutorial we will implement a client API using Swift Meta Types, Protocols with associated types, protocol inheritance, enums with associated values and of course generics, if any of this topics is not familiar to you I suggest stop here and check these resources.

Types and Meta Types in Swift.

Generics in Swift.

Enum With Associated values.

Protocols with Associated Types (PAT).

Ok cool, let’s start by cloning or downloading this repo, Explore the project and you can see it contains different folders. The “Protocols” and “Networking” folders contain implementations that I shared before and that you can reuse for almost every Client API implementation, I won't get in details about that but you can see what's going on there in detail in this 2 posts.

Protocol Based Generic Networking using JSONDecoder and Decodable in Swift 4

Protocol-Based Generic Networking -Part 2 JSONEncoder and Encodable for Post request in Swift.

I also added a “Models” folder it contains every Resource available in the SWAPI API such as Films, People, Spaceships and so on, I also recommend taking a look of the SWAPI API documentation to understand its structure.

In the “SwapiWrapper” folder you can see three files, it contains all the final implementation (commented by now) we will uncomment while we are moving forward.

Let’s start with “SwapiEndpoint”, if you saw the Star wars API docs you can see that its very easy to use and it contains different paths for different types of resources, you can get lists of Planets and also you can search for a Spaceship using an ID or a name query, yes basically pretty much all the content on the Star Wars ecosystem!

enum SwapiEndpoint {    case people(_ id: String?, query: String?)    case films(_ id: String?, query: String?)    case planets(_ id: String?, query: String?)    case species(_ id: String?, query: String?)    case starships(_ id: String?, query: String?)    case vehicles(_ id: String?, query: String?)
}

This enum will be the one that handles the endpoints that we will need to direct the requests, let's make now this enum conform to Endpoint protocol (the one in Networking file).

extension SwapiEndpoint: Endpoint {
var base: String { return "https://swapi.co" // this is the base URL } var path: String { switch self { case .people(let id, _): return "/api/people/\(id ?? "")" case .films(let id, _): return "/api/films/\(id ?? "")" case .planets(let id, _): return "/api/planets/\(id ?? "")" case .species(let id, _): return "/api/species/\(id ?? "")" case .starships(let id,_): return "/api/starships/\(id ?? "")" case .vehicles(let id, _): return "/api/vehicles/\(id ?? "")" } }
var queryItems: [URLQueryItem] { var q = "" switch self { case .people(_, let query): q = query ?? "" case .films(_, let query): q = query ?? "" case .planets(_, let query): q = query ?? "" case .species(_, let query): q = query ?? "" case .starships(_, let query): q = query ?? "" case .vehicles(_, let query): q = query ?? "" } return [URLQueryItem(name: "search", value: q)] }}

By conforming to Endpoint protocol or any other protocol you need to provide an implementation for every property defined on that protocol, maybe you can do all of this without needing a protocol but I think its good to define one to set rules about what is required in a certain model. Now inside this extension, we also have a custom init that uses the Models Meta types to instantiate our enum.

init?(T: Decodable.Type, id: String?, query: String?) {        switch T {        case is Film.Type: self = .films(id, query: query)        case is People.Type: self = .people(id, query: query)        case is Starship.Type: self = .starships(id, query: query)        case is Vehicle.Type: self = .vehicles(id, query: query)        case is Species.Type: self = .species(id, query: query)        case is Planet.Type: self = .planets(id, query: query)        default: return nil        }    }

When using generic functions for our client, we will pass in as arguments the meta type of a model, when that happens we will use it to instantiate our enum and get the correct path based on the model's type.

Now let’s go to “Resources” file, you can see a protocol like this…

protocol Resource: Decodable {    associatedtype T     var count: Int? { get }    var next: String? { get }    var previous: String? { get }    var results: [T]? { get }}

This is a protocol with Associated Types and it is how you define generics in protocols. It has as an associated type of type “T” we will define “T” next, but first you are maybe asking what is the count, next and previous properties about, well its how SWAPI returns the data when you perform a request for a full list of resources, the payload contains a dictionary with a “count”, “next”, “previous” and “results” keys, something like this…

{
"count": 87,
"next": "https://swapi.co/api/people/?page=2",
"previous": null,
"results": [
{
"name": "Luke Skywalker",
"height": "172",
"mass": "77",
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "https://swapi.co/api/planets/1/",
"films": [
"https://swapi.co/api/films/2/",
"https://swapi.co/api/films/6/",
"https://swapi.co/api/films/3/",
"https://swapi.co/api/films/1/",
"https://swapi.co/api/films/7/"
],
"species": [
"https://swapi.co/api/species/1/"
],
"vehicles": [
"https://swapi.co/api/vehicles/14/",
"https://swapi.co/api/vehicles/30/"
],
"starships": [
"https://swapi.co/api/starships/12/",
"https://swapi.co/api/starships/22/"
],
"created": "2014-12-09T13:50:51.644000Z",
"edited": "2014-12-20T21:17:56.891000Z",
"url": "https://swapi.co/api/people/1/"
},
{
"name": "C-3PO",
"height": "167",
"mass": "75",
"hair_color": "n/a",
"skin_color": "gold",
"eye_color": "yellow",
"birth_year": "112BBY",
"gender": "n/a",
"homeworld": "https://swapi.co/api/planets/1/",
"films": [
"https://swapi.co/api/films/2/",
"https://swapi.co/api/films/5/",
"https://swapi.co/api/films/4/",
"https://swapi.co/api/films/6/",
"https://swapi.co/api/films/3/",
"https://swapi.co/api/films/1/"
],
"species": [
"https://swapi.co/api/species/2/"
],
"vehicles": [],
"starships": [],
"created": "2014-12-10T15:10:51.357000Z",
"edited": "2014-12-20T21:17:50.309000Z",
"url": "https://swapi.co/api/people/2/"
}
.... 85 other Awesome Star wars Characters goes here..

Now we will use protocol inheritance to create a model that conforms to Resource that will Decode the payload.

public struct Resources<T: Decodable>: Resource {    var count: Int?
var next: String?
var previous: String?
var results: [T]?
}

As you can see this model conforms to Resource which conforms to Decodable so this means that this model also conforms to Decodable and can be used to decode JSON, it has a generic type constraint also of type Decodable and its how we define the type for the Resource associated type “T”… what that this mean? well, it means that this object conforms to Decodable but also holds an array of models that conform to Decodable.

You may be asking why we do not simplify this with something like this…

public struct Resource<T: Decodable>: Decodable {    var count: Int?
var next: String?
var previous: String?
var results: [T]?
}

And it's a fair question, why introduce protocol with associated types and protocol inheritance? well, when I show you the functions on the client API it will maybe make more sense, I hope so.

So why all this trouble in the first place? Well let’s say I wanted to do this without generics or protocols and I want a list of people from Star Wars movies using the payload that I shared a few moments ago, I will have to do something like this…

https://swapi.co/api/people
public struct PeopleResults: Decodable {
var count: Int?
var next: String?
var previous: String?
var results: [People]? // People conforms to Decodable.
}

This works great however when I made another request lets say now for planets I will have to do something like this because the payload looked the same and the only difference was the type in the results array.

https://swapi.co/api/planetspublic struct PlanetResults: Decodable {    var count: Int?
var next: String?
var previous: String?
var results: [Planet]? // planet conforms to Decodable.
}

I will have to do this every time for every results type, by using generics I avoided all of that and now I can handle every type without creating a new model.

Finally, let’s use all our code to create the Client API, go to “Vader” file (of course I wanted to give it a cool name)…

class Vader: GenericAPI {
var session: URLSession init(configuration: URLSessionConfiguration) { self.session = URLSession(configuration: configuration) }
public convenience init() { self.init(configuration: .default) }
private func fetch<T: Decodable>(with request: URLRequest, completion: @escaping (Result<T?, APIError>) -> Void) { fetch(with: request, decode: { json -> T? in guard let resource = json as? T else { return nil } return resource }, completion: completion) }
}

This API conforms to Generic API (the one in the Networking folder) it has an initializer and also a generic method that takes a request and decodes a model as long it conforms to Decodable, we will add three methods here and it will be ALL that we need to be able to fetch every different type of results like for example a list of People or a specific people by searching by ID or “Name” …

// Get a list of resources of any type    public func get<T: Resource>(_ value: T.Type, completion: @escaping (Result<T?, APIError>) -> Void) {
guard let decodedAssociatedType = value.T.self as? Decodable.Type else { return } guard let resource = SwapiEndpoint(T: decodedAssociatedType, id: nil, query: nil) else { return } let request = resource.request self.fetch(with: request, completion: completion) }
// Get a Resource of any type searching by ID ex. "1" will return "Luke Skywalker" public func search<T: Decodable>(_ value: T.Type, withID id: String, completion: @escaping (Result<T?, APIError>) -> Void) {
guard let resource = SwapiEndpoint(T: value, id: id, query: nil) else { return } let request = resource.request self.fetch(with: request, completion: completion) }// Get a Resource of any type searching by Nanme ex. "r2" will return "R2-D2"
// Why I need this method takes a Resource type constraint and not just Decodable? because the payload for this has the same structure as a List resources, please check the SWAPI docs.
public func search<T: Resource>(_ value: T.Type, query: String, completion: @escaping (Result<T?, APIError>) -> Void) {
guard let decodedAssociatedType = value.T.self as? Decodable.Type else { return } guard let resource = SwapiEndpoint(T: decodedAssociatedType, id: nil, query: query) else { return } let request = resource.request self.fetch(with: request, completion: completion) }

Remember when we introduce protocol inheritance and protocols with associated types, well if you take a look on the functions with a Resource type constraint you can see that we are decoding the type but also accessing its associated value in one line of code…

guard let decodedAssociatedType = value.T.self as? Decodable.Type else { return }

where value can be of type “Resources<Starship>” , and value.T is in this case “Starship”.

So again why going for all this trouble? well, let's think again how we implement API’s usually you will have something like this…

func getStarshipsWith(completion: @escaping (Result<Starship?, APIError>) -> Void) {
// completion()
}

then repeat the same for another model type…

func getPlanetssWith(completion: @escaping (Result<Planet?, APIError>) -> Void) {
// completion()
}

and so on for every type defining the type in the function signature and modifying the type for every different result plus implementing the methods for search etc. By mixing all these Swift features, now you only need three methods on your API and you can just reuse them for every different type, let's see how it looks like fetching Starships and Planets using the same function, go to ViewController file and uncomment the code…

// Get a list of Starships    
Vader().get(Resources<Starship>.self) {
}
// Get a list of Planets
Vader().get(Resources<Planet>.self) {
}

The same function for different models!

Here we are not only taking the best of Swift in our advantage but we are also following the Swift guidelines by making this functions readable when are used.

I hope you find this helpful.

Big shout out again to Paul Hallett for implementing the SWAPI API, thank you very much!

Happy holidays!

This story is published in Noteworthy, where 10,000+ readers come every day to learn about the people & ideas shaping the products we love.

Follow our publication to see more product & design stories featured by the Journal team.

--

--