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
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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.
  """
}