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
Interacting with APIs usually requires a lot of networking code. You can solve this problem by abstracting all of that and calling Swift functions to get the results. In this tutorial you’ll learn how to create an API helper library using SwiftNIO’s Futures and Promises.
Getting Started
Before you start the project, you should understand what Futures and Promises are. When you transmit data over the internet, as with an API request, the data doesn’t arrive instantly. Your program has to wait for all the data to arrive from the API before it can continue with its work.
In theory, you could stop your app’s execution and wait until all the data has arrived. But that’s generally a bad idea because no other requests can process during that time.
Instead, you can use Futures and Promises to indicate that while your data isn’t there yet, it will be in the future. This allows you to work asynchronously and continue to process other requests while you wait.
In this project, you’ll use SwiftNIO, a low level networking library created by Apple. It’ll provide you with Futures, Promises and everything else you need to create an API helper library.
You can compare a future to inviting someone over for dinner. You know they’ll arrive at some point in the future. This means you can start preparing now, so when they arrive, you can have dinner right away.
Futures in SwiftNIO work in the same fashion. They indicate something will happen in the future. You provide the callback up front and then the library executes that callback at the appropriate time.
Creating the Project
Now that’s out of the way, check out the project. To download the starter project click the Download Materials button at the top or bottom of this tutorial.
In this tutorial, you’ll create a helper for the Star Wars API, or SWAPI for short. The starter project already contains a basic networking client, some DateFormatter
extensions and the start of the main SWAPI class.
Once downloaded, navigate to the Starter folder. Open your terminal and generate and open the Xcode project by running:
swift package generate-xcodeproj && xed .
Make sure you’ve selected the API-Awakens scheme and set the run device to My Mac.
Hit Run to run the project. You should see the following message in the console:
Let's get started building the SWAPI client!
Implementing the SWAPI API
With the basic project up and running, it’s time to add some functionality! First, you’ll create a Swift Struct to reflect each of the resources returned by the SWAPI.
The first resource is a Person. You can find the documentation, including schema, here.
You’ll store the models in a new folder called models. In Xcode, create the models under the swapi folder in the project. Your file tree should now look like this:
Inside this new models folder, create a file called Person.swift and replace its contents with the following:
import Foundation
import NIO
// 1
public struct Person: Codable {
// 2
public let name: String
public let birthYear: String
public let eyeColor: String
public let gender: String
public let hairColor: String
public let height: String
public let mass: String
public let skinColor: String
private let _homeworld: String
private let _films: [String]
private let _species: [String]
private let _starships: [String]
private let _vehicles: [String]
public let url: String
public let created: Date
public let edited: Date
// 3
enum CodingKeys: String, CodingKey {
case birthYear = "birth_year",
name,
mass,
_vehicles = "vehicles",
height
case hairColor = "hair_color",
skinColor = "skin_color",
_starships = "starships"
case created,
eyeColor = "eye_color",
gender,
_homeworld = "homeworld",
_species = "species"
case url,
edited,
_films = "films"
}
}
Here’s what’s going on:
- First, you create a
Person
struct and conform it toCodable
. - Next, you create a property for every property found in the SWAPI docs.
- Finally, you create a
CodingKeys
enum. This enum will tell your decoder what key it should look for in the JSON response.
You might notice that all the properties here are public except for the ones prefixed with an underscore. This is because those properties don’t contain any data themselves, but instead point to another resource. Later on, you’ll add helper methods to get those values.
Now that you have the Person
struct, you can create the others. But instead of copy pasting all of them, open Finder, navigate to the Starter folder and move all files in models to Sources/swapi/models. Once you’ve done this you’ll have to close your Xcode project and regenerate it using:
swift package generate-xcodeproj && xed .
You have to regenerate your project so Xcode can pick up the new files you added.
The workspace file at “/Users/lotu/Downloads/API-Awakens/Starter/API-Awakens.xcodeproj/project.xcworkspace” has been modified by another application.
, click the Revert button.If you click through the new model files you added, you’ll see they all have the same setup as the Person
file.
Next, start on your base API class. Open Swapi.swift and replace the contents of the SwapiClient
class with the following:
// 1
public let worker: EventLoopGroup
// 2
public let session: URLSession
// 3
private let decoder: JSONDecoder
// 4
public init(worker: EventLoopGroup) {
self.worker = worker
session = URLSession(configuration: .default)
decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom(DateFormatter.customDecoder)
}
Here’s what’s going on:
- The
EventLoopGroup
from NIO will create your Futures & Promises. - The shared
URLSession
you’ll use for your networking. - The shared
JSONDecoder
you’ll use for decoding your resources. - The initializer setting all the above mentioned properties. This initializer also sets the
dateDecodingStrategy
of the decoder to a custom strategy which you can find in the Utils/DateFormatter.swift file.
With this change, your project won’t build anymore because of an error in main.swift. Open main.swift and replace the print statement with the following:
let client = SwapiClient(worker: eventLoopGroup)
Build the project to make sure there are no more errors.
Connecting to the SWAPI
Now that the base of your project is functional, you need to connect to the SWAPI. First, create a new file called SwapiURLBuilder.swift in the Sources/swapi folder. In this file you’ll create a helper that will construct URLs you can pass to the networking client.
swapi
target when adding the file.Finally, replace the contents of the file with the following:
import Foundation
// 1
enum Resource: String {
case people, films, starships
case vehicles, species, planets
}
// 2
enum SwapiURLBuilder {
// 3
static let baseUrl = "https://swapi.dev/api"
// 4
static func buildUrl(for resource: Resource, withId id: Int? = nil) -> URL? {
var urlString = baseUrl + "/\(resource.rawValue)"
if let id = id {
urlString += "/\(id)"
}
return URL(string: urlString)
}
}
Here’s a breakdown of what you added:
- An enum holding all the resources the SWAPI supports.
- An empty enum which is the actual URL Builder. It’s empty because you don’t want it initialize.
- The base URL of the SWAPI.
- A static method to build a URL based on the
Resource
passed in and optionally an ID.
Now that you can construct URLs for your resources, you can add a method to your SwapiClient
class to retrieve one. Open Swapi.swift, and add the following to the end of SwapiClient
just before the closing curly brace:
func get<R>(_ route: URL?) -> EventLoopFuture<R> where R: Decodable {
guard let route = route else {
return worker.next().makeFailedFuture(URLSessionFutureError.invalidUrl)
}
return session.jsonBody(
URLRequest(route, method: .GET),
decoder: decoder,
on: worker.next())
}
This method takes in a URL and returns a Future
holding your resource.
This is nice, but requires users to create URLs themselves. To resolve this you’ll add extension methods to SwapiClient
for every resource to get the URLs by passing in an ID.
First, open Film.swift and add the following to the end of the file:
public extension SwapiClient {
func getFilm(withId id: Int) -> EventLoopFuture<Film> {
return self.get(SwapiURLBuilder.buildUrl(for: .films, withId: id))
}
}
Next, open Person.swift and add the following to the end of the file:
public extension SwapiClient {
func getPerson(withId id: Int) -> EventLoopFuture<Person> {
return self.get(SwapiURLBuilder.buildUrl(for: .people, withId: id))
}
}
Next, open Planet.swift and add the following to the end of the file:
public extension SwapiClient {
func getPlanet(withId id: Int) -> EventLoopFuture<Planet> {
return self.get(SwapiURLBuilder.buildUrl(for: .planets, withId: id))
}
}
Next, open Species.swift and add the following to the end of the file:
public extension SwapiClient {
func getSpecies(withId id: Int) -> EventLoopFuture<Species> {
return self.get(SwapiURLBuilder.buildUrl(for: .species, withId: id))
}
}
Next, open Starship.swift and add the following to the end of the file:
public extension SwapiClient {
func getStarship(withId id: Int) -> EventLoopFuture<Starship> {
return self.get(SwapiURLBuilder.buildUrl(for: .starships, withId: id))
}
}
Finally, open Vehicle.swift and add the following to the end of the file:
public extension SwapiClient {
func getVehicle(withId id: Int) -> EventLoopFuture<Vehicle> {
return self.get(SwapiURLBuilder.buildUrl(for: .vehicles, withId: id))
}
}
Just like that you’ve created an API wrapper! To try it out, open main.swift and add the following to the end of the file:
let person = try client.getPerson(withId: 1).wait()
print(person.name)
print("===\n")
let film = try client.getFilm(withId: 1).wait()
print(film.title)
print("===\n")
let starship = try client.getStarship(withId: 9).wait()
print(starship.name)
print("===\n")
let vehicle = try client.getVehicle(withId: 4).wait()
print(vehicle.name)
print("===\n")
let species = try client.getSpecies(withId: 3).wait()
print(species.name)
print("===\n")
let planet = try client.getPlanet(withId: 1).wait()
print(planet.name)
Run your program. In the console you should see the following:
Luke Skywalker === A New Hope === Death Star === Sand Crawler === Wookiee === Tatooine
Awesome!