

Creating an API Helper Library for SwiftNIO [FREE]
source link: https://www.raywenderlich.com/4623672-creating-an-api-helper-library-for-swiftnio
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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 .

Target & Run destination
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.
Note
: If Xcode gives an error like 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.
Note
: Make sure you have selected the 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.co/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:
Resource

Connect to the SWAPI, you must.
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!
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(_ route: URL?) -> EventLoopFuture 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. """ }
Using the New Methods
You’ve survived the copy and paste storm! All that’s left now is to use the awesome new methods you added! Open main.swift
and replace everything below let client = SwapiClient(worker: eventLoopGroup)
with the following:
let person = try client.getPerson(withId: 1).wait() let details = try person.personalDetails.wait() print(details) print("===\n") let film = try client.getFilm(withId: 1).wait() print(film.info) print("===\n") let starship = try client.getStarship(withId: 9).wait() print(starship.info) print("===\n") let vehicle = try client.getVehicle(withId: 4).wait() print(vehicle.info) print("===\n") let species = try client.getSpecies(withId: 3).wait() let speciesInfo = try species.info.wait() print(speciesInfo) print("===\n") let planet = try client.getPlanet(withId: 1).wait() print(planet.info)
Build and Run the application and there you have it. You created your very own API wrapper using SwiftNIO!
Where to Go From Here
You can download the final project using the Download Materials button at the top or bottom of this page.
To learn more about SwiftNIO and how to use it, take a look at these tutorials for a simple guide to async on the server or creating a TCP server with SwiftNIO . If you want to explore even further, the official NIO Docs are a great place to start.
Need a challenge? Try extending your API wrapper by adding even more functionality. For example, you could add SWAPI’s Wookiee mode .
If you have any comments or questions, please join the forum below!
Recommend
-
61
苹果开源Swift底层非阻塞I/O框架SwiftNIO
-
31
苹果公司开源的Swift版Netty:SwiftNIO
-
62
README.md SwiftNIO HTTP/2 This project contains HTTP/2 support for Swift projects using SwiftNIO. Please be aware that this project is cu...
-
46
README.md SwiftNIOMock A web server based on SwiftNIO designed to be used as a mock server in UI autom...
-
46
An important topic in server-side Swift and SwiftNIO is asynchronous programming . Asynchronous programming is a way of programming that enables the program to work on multiple tasks in parallel by switching...
-
32
README.md SwiftNIO SSL SwiftNIO SSL is a Swift package that contains an implementation of TLS based on BoringSSL. This package allows users o...
-
56
README.md
-
35
README.md AsyncHTTPClient This package provides simple HTTP Client library built on top of SwiftNIO. This library provides the followi...
-
5
Download library source and cookbook examples - 12.7 KB
-
3
What's new in the Twilio helper library for ASP.NET (v6.0.0 - August 2022)
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK