Server-Side Swift Beta

Learn web development with Swift, using Vapor and Kitura.

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.

4/5 3 Ratings

Version

  • Swift 5, macOS 10.14, Xcode 11

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.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:

  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!

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 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!

Average Rating

4/5

Add a rating for this content

3 ratings

Contributors

Comments