Creating an API Helper Library for SwiftNIO
In this SwiftNIO tutorial you’ll learn how to utilize the helper types from SwiftNIO to create an API library that accesses the Star Wars API. By Jari Koopman.
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
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
Creating an API Helper Library for SwiftNIO
20 mins
Extending the Functionality
Remember when you created the models, you also created a few private properties prefixed with an underscore? Time to add some computed properties to your resources to get the related objects.
Setup the Extendability
To achieve this, you first have to add a few more extension methods to SwapiClient
. Namely, you need to add one that can retrieve a list of resources based on a list of URLs.
First, open Film.swift and add the following to the end of the extension SwapiClient
:
func getFilms(withUrls urls: [String]) -> EventLoopFuture<[Film]> {
return EventLoopFuture.whenAllSucceed(
urls
.compactMap { URL(string: $0) }
.map { self.get($0) }
, on: worker.next())
}
Next, open Person.swift and add the following to the end of the extension SwapiClient
:
func getPeople(withUrls urls: [String]) -> EventLoopFuture<[Person]> {
return EventLoopFuture.whenAllSucceed(
urls
.compactMap { URL(string: $0) }
.map { self.get($0) }
, on: worker.next())
}
Next, open Planet.swift and add the following to the end of the extension SwapiClient
:
func getPlanets(withUrls urls: [String]) -> EventLoopFuture<[Planet]> {
return EventLoopFuture.whenAllSucceed(
urls
.compactMap { URL(string: $0) }
.map { self.get($0) }
, on: worker.next())
}
Next, open Species.swift and add the following to the end of the extension SwapiClient
:
func getSpecies(withUrls urls: [String]) -> EventLoopFuture<[Species]> {
return EventLoopFuture.whenAllSucceed(
urls
.compactMap { URL(string: $0) }
.map { self.get($0) }
, on: worker.next())
}
Next, open Starship.swift and add the following to the end of the extension SwapiClient
:
func getStarships(withUrls urls: [String]) -> EventLoopFuture<[Starship]> {
return EventLoopFuture.whenAllSucceed(
urls
.compactMap { URL(string: $0) }
.map { self.get($0) }
, on: worker.next())
}
Finally, open Vehicle.swift and add the following to the end of the extension SwapiClient
:
func getVehicles(withUrls urls: [String]) -> EventLoopFuture<[Vehicle]> {
return EventLoopFuture.whenAllSucceed(
urls
.compactMap { URL(string: $0) }
.map { self.get($0) }
, on: worker.next())
}
Each of these little snippets takes an Array of Strings and turns it in a Future holding an Array of one of your resource models. You use one of NIO’s helper methods that takes an Array of Future
and turns it into a Future<[T]>
. Pretty awesome, right?
Next, you have to give your resources access to your SwapiClient
to get their related objects. Create a new file in the models folder called SwapiModel.swift and replace its contents with the following:
protocol SwapiModel {
var client: SwapiClient! { get set }
}
Now go into each model file and conform the struct to SwapiModel
. You’ll also have to add the property to each model. While doing this, make sure the property is weak
to prevent reference cycles.
Your models should now look like this:
struct Model: Codable, SwapiModel {
weak var client: SwapiClient!
// Rest of the code
}
Finally, open Swapi.swift and replace get(_:)
with the following:
func get<R>(_ route: URL?) -> EventLoopFuture<R> where R: Decodable & SwapiModel {
guard let route = route else {
return worker.next().makeFailedFuture(URLSessionFutureError.invalidUrl)
}
return session.jsonBody(
URLRequest(route, method: .GET),
decoder: decoder,
on: worker.next())
.map({ (result: R) in
var result = result
result.client = self
return result
})
}
The above code ensures the return value conforms to both Decodable
and SwapiModel
. It also sets the model’s client to self
in the map
body.
Extending the Models
With all the preparation out of the way, now you can add helpers to your resource models. First, open Film.swift and add the following code below the CodingKeys
enum:
public var species: EventLoopFuture<[Species]> {
return client.getSpecies(withUrls: _species)
}
public var starships: EventLoopFuture<[Starship]> {
return client.getStarships(withUrls: _starships)
}
public var vehicles: EventLoopFuture<[Vehicle]> {
return client.getVehicles(withUrls: _vehicles)
}
public var characters: EventLoopFuture<[Person]> {
return client.getPeople(withUrls: _characters)
}
public var planets: EventLoopFuture<[Planet]> {
return client.getPlanets(withUrls: _planets)
}
public var info: String {
return """
\(title) (EP \(episodeId)) was released at \(DateFormatter.yyyyMMdd.string(from: releaseDate)).
The film was directed by \(director) and produced by \(producer).
The film stars \(_species.count) species, \(_planets.count) planets, \(_starships.count + _vehicles.count) vehicles & starships and \(_characters.count) characters.
"""
}
For every private, underscored property you decoded from the JSON you now have a user facing property which returns a Future. You also have the info
property which gives some well formatted information about the film.
Next, you’ll add these user facing properties to all the other models. Prepare for copy and paste madness!
Next, open Person.swift, and add the following below the CodingKeys
enum:
public var films: EventLoopFuture<[Film]> {
return client.getFilms(withUrls: _films)
}
public var species: EventLoopFuture<[Species]> {
return client.getSpecies(withUrls: _species)
}
public var starships: EventLoopFuture<[Starship]> {
return client.getStarships(withUrls: _starships)
}
public var vehicles: EventLoopFuture<[Vehicle]> {
return client.getVehicles(withUrls: _vehicles)
}
public var homeworld: EventLoopFuture<Planet> {
return client.get(URL(string: _homeworld))
}
public var personalDetails: EventLoopFuture<String> {
return homeworld.map { planet in
return """
Hi! My name is \(self.name). I'm from \(planet.name).
We live there with \(planet.population) people.
I was born in \(self.birthYear), am \(self.height) CM tall and weigh \(self.mass) KG.
"""
}
}
As with the Films, this adds your user facing properties and a formatted info string.
Open Planet.swift and add the following below the CodingKeys
enum:
public var films: EventLoopFuture<[Film]> {
return client.getFilms(withUrls: _films)
}
public var residents: EventLoopFuture<[Person]> {
return client.getPeople(withUrls: _residents)
}
public var info: String {
return """
\(name) is a \(climate) planet. It's orbit takes \(orbitalPeriod) days, and it rotates around its own axis in \(rotationPeriod) days.
The gravity compared to Earth is: \(gravity). The planet has a diameter of \(diameter) KM and an average population of \(population).
"""
}
Next, open Species.swift and add the following below the CodingKeys
enum:
public var people: EventLoopFuture<[Person]> {
return client.getPeople(withUrls: _people)
}
public var films: EventLoopFuture<[Film]> {
return client.getFilms(withUrls: _films)
}
public var homeworld: EventLoopFuture<Planet> {
return client.get(URL(string: _homeworld ?? ""))
}
public var info: EventLoopFuture<String> {
return homeworld.map { planet in
return """
The \(self.name) are a \(self.classification) species living on \(planet.name).
They are an average of \(self.averageHeight) CM tall and live about \(self.averageLifespan) years.
They speak \(self.language) and are a \(self.designation) species.
"""
}
}
Next, open Starships.swift and add the following below the CodingKeys
enum:
public var films: EventLoopFuture<[Film]> {
return client.getFilms(withUrls: _films)
}
public var pilots: EventLoopFuture<[Person]> {
return client.getPeople(withUrls: _pilots)
}
public var info: String {
return """
The \(name) (\(model)) is a \(starshipClass) created by \(manufacturer).
It holds \(passengers) passengers and \(crew) crew.
The \(name) is \(length) meters long and can transport \(cargoCapacity) KG worth of cargo.
"""
}
Finally, open Vehicle.swift and add the following below the CodingKeys
enum:
public var films: EventLoopFuture<[Film]> {
return client.getFilms(withUrls: _films)
}
public var pilots: EventLoopFuture<[Person]> {
return client.getPeople(withUrls: _pilots)
}
public var info: String {
return """
The \(name) (\(model)) is a \(vehicleClass) created by \(manufacturer).
It holds \(passengers) passengers and \(crew) crew.
The \(name) is \(length) meters long and can transport \(cargoCapacity) KG worth of cargo.
"""
}