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.

Leave a rating/review
Download materials
Save for later
Share

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.

Swift Cooking

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:

  1. First, you create a Person struct and conform it to Codable.
  2. Next, you create a property for every property found in the SWAPI docs.
  3. 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:

  1. The EventLoopGroup from NIO will create your Futures & Promises.
  2. The shared URLSession you’ll use for your networking.
  3. The shared JSONDecoder you’ll use for decoding your resources.
  4. 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.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:

  1. An enum holding all the resources the SWAPI supports.
  2. An empty enum which is the actual URL Builder. It’s empty because you don’t want it initialize.
  3. The base URL of the SWAPI.
  4. A static method to build a URL based on the Resource passed in and optionally an ID.

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!