How to Build API Driven iOS Apps in Swift Using Siesta

August 08, 2016
Written by

siesta-header

There are so many wonderful APIs out there but they aren’t always easy to work with in mobile apps. The Siesta framework makes using APIs in Swift apps a joy.

Patterns for using REST APIs in mobile apps tend to focus on requests but the ‘R’ that matters most in REST is Resource. Siesta provides an app-wide observable model of a RESTful resource’s state which answers the following three questions:

  • What is the latest data for this resource, if any?
  • Did the latest request result in an error?
  • Is there a request in progress?

Not only does it answer those questions but as soon as one changes it broadcasts answers to anything interested in listening to them. It also caches the results so you don’t make the same network request twice unless you need to.

Since everyone’s still going crazy about Pokémon Go we’ll build an API-driven Pokédex in this post. We’ll use the Pokéapi to build our Pokédex. When it’s done it’ll look like this:

pokedex

Let’s get started so we can catch ‘em all.

What You’ll Need

Before we can get started you’ll want to make sure you have some prerequisites ready to go. Here’s a list of what you’ll need for the Pokédex:

  1. Xcode 7 (latest version at time of writing is 7.3.1)
  2. Swift 2.2 (this is the version that ships with Xcode 7.3.1)
  3. CocoaPods for managing dependencies
  4. This starter project from Github which contains some pre-built user interface screens and project structure to save some work in this tutorial. We’ll clone this in the next step.

Setting Up the Project

To get started, clone the starter project repo:

git clone https://github.com/brentschooley/PokeapiDex.git

The starter project contains a Podfile that describes the CocoaPods dependencies our project needs to include. The two dependencies we need in this app are:

Run the following command in the starter project’s directory to install the dependencies using CocoaPods:

pod install

Note: If you get an error trying to install the pods you may need to run pod repo update to refresh the local CocoaPods spec repo. This might take a while.

If you want to know more about CocoaPods you can read this blog post. Open the PokeapiDex.xcworkspace file that CocoaPods in Xcode by running open PokeapiDex.xcworkspace. Build the project once before we get started to resolve the dependencies inside of Xcode. If you don’t do this Xcode won’t provide completions for the dependencies we added using CocoaPods.

Now that we have the Siesta framework for working with APIs and SwiftyJSON for easier JSON processing we can dive into creating our Pokédex.

Creating a Siesta Service

A Service in the Siesta framework represents an API. We need to create one to represent the Pokeapi. Create a new iOS Swift file (iOS->Source->Swift) in the API folder called Pokeapi.swift. We’ll create the Pokeapi Service as a singleton to be used anywhere in the project. We’ll use a private init() function so that _Pokeapi can only be instantiated from this source file.

Replace the contents of Pokeapi.swift with the following code:

import Foundation
import Siesta
import SwiftyJSON

let Pokeapi = _Pokeapi()

class _Pokeapi: Service {
    private init() {
        super.init(baseURL: "https://pokeapi.co/api/v2")
        
        // Configuration
        self.configure {
            $0.config.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
            $0.config.expirationTime = 3600
        }
    }
}

private let SwiftyJSONTransformer =
    ResponseContentTransformer
        { JSON($0.content as AnyObject) }

This code creates a Service representing the API found at https://pokeapi.co/api/v2. It configures the service to parse responses from the API using SwiftyJSON by hooking into Siesta’s robust transformer pipeline. We also set the expiration time of the cache to 1 hour for all resources since our data won’t actually change. The default of 30 seconds makes much more sense for an API which has contents that change frequently.

With our Service created we can start working with the resources that are available in the Pokeapi.

Setting Up Siesta Resources

A Resource is a local cache of a RESTful resource from the API. It holds the data for the resource as well as status details about the network requests related to it. We’ll use Resource objects to fetch info about the first 151 Pokemon in the Pokeapi. First, update the init() method in the _Pokeapi() Service to configure a transformer for the /pokemon endpoint of the Pokeapi by adding the highlighted code:


private init() {
    super.init(baseURL: "https://pokeapi.co/api/v2")
    
    // Configuration
    self.configure {
        $0.config.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
        $0.config.expirationTime = 3600
    }
    
    self.configureTransformer("/pokemon") {
        ($0.content as JSON)["results"].arrayValue
    }
}

The highlighted code takes the response from any request to /pokemon and returns the SwiftyJSON array representation of the results value. Next let’s add a Resource property to our Service called pokedex. Put this code after the closing tag for the init method:

var pokedex: Resource { return resource("/pokemon").withParam("limit", "151") }

This Resource property will fetch the first 151 Pokémon from the Pokeapi and make them available to any object that observes them. The PokedexViewController will use this Resource to display the list of Pokémon in a UITableView. Let’s wire that up now so we can see Siesta in action.

Gotta List ‘em All

Now that we have a Resource that returns JSON, let’s display it in our PokedexViewController. Head over to PokedexViewController.swift and replace the placeholder line that says var objects = [AnyObject]() with the following code:

var pokemonList: [JSON] = [] {
    didSet {
        tableView.reloadData()
    }
}

The pokemonList variable will be loaded on a background thread so we use didSet to reload our table view once the list has been loaded. Siesta has a UI helper called ResourceStatusOverlay that will provide a progress indicator when the list is being fetched as well as a mechanism to retry a request if it fails. Let’s add that to the top of PokedexViewController:

var statusOverlay = ResourceStatusOverlay()

override func viewDidLayoutSubviews() {
    statusOverlay.positionToCoverParent()
}

Then, add the following highlighted line to viewDidLoad:


override func viewDidLoad() {
    super.viewDidLoad()
    statusOverlay.embedIn(self)
}

Now we’ll hook up the pokedex Resource to populate our pokemonList array. Add the following code to the top of the PokedexViewController class making sure to add the ResourceObserver protocol to the class declaration:

class PokedexViewController: UITableViewController, ResourceObserver {
    var pokedexResource: Resource? {
        didSet {
            oldValue?.removeObservers(ownedBy: self)
            
            pokedexResource?
                .addObserver(self)
                .addObserver(statusOverlay, owner: self)
                .loadIfNeeded()
        }
    }
    
    func resourceChanged(resource: Resource, event: ResourceEvent) {
        pokemonList = pokedexResource?.typedContent() ?? []
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        statusOverlay.embedIn(self)
        pokedexResource = Pokeapi.pokedex
    }
    
    // …
    
}

When the pokedexResource property is set it does the following things:

  • Removes any existing observers
  • Adds self and the status overlay as observers
  • Loads the data for the resource if needed based on the cache expiration timeout

The resourceChanged() function will be called when the resource broadcasts a change event. This usually means new data is available. The typedContent() function is a convenience method that returns a type-casted value for the latest result for the Resource if available or nil if it’s not. In our case it’s a SwiftyJSON array of Pokémon ready to be displayed in the table view. In viewDidLoad we set the pokedexResource property to the Pokeapi.pokedex resource we declared in our service which kicks the whole resource fetching process off.

Now we can display the list in our tableView. Replace the table view functions with the following code:

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return 1
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return pokemonList.count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("PokemonCell", forIndexPath: indexPath)
    
    // Get pokemon out of pokemon resource
    let pokemonSummary = pokemonList[indexPath.row]
    
    // Customize cell here based on pokemon
    cell.textLabel?.text = pokemonSummary["name"].stringValue.capitalizedString
    cell.detailTextLabel?.text = "id: \(indexPath.row + 1)"
    
    return cell
}

This is standard UITableViewController code. We have one section that contains a number of rows set to the number of Pokémon in pokemonList. In the tableView:cellForRowAtIndexPath function we get the Pokémon summary JSON that corresponds with that row. We then use SwiftyJSON to get the name out of the JSON and capitalize it and set it as textLabel.text. Then we display the id of the Pokémon in detailTextLabel.text.

Build the app and you should see a loading indicator while Siesta fetches the Pokémon list and then you will see this:

(Because of high demand, the PokéApi might be slow to respond and it has gone down a few times. You might have to be patient and try the request again.)
Now that we have a list of Pokémon it would be really great to see more details about them. First we’re going to need to create a data model for a Pokémon.

Gotta Model ‘em All

We built the Pokémon list using JSON data because we only had one field to work with. For our Pokémon details screen we’re going to be working with more properties so let’s create a model for the Pokémon data. The Pokeapi is going to return us data in JSON format and we’ll want to represent that data in Swift data structures.

Create a new Swift File in the Models folder called Pokemon.swift. For each Pokemon we will store the name, sprite image URL and the two types the Pokemon is characterized by (grass, poison, etc.). Replace the contents of Pokemon.swift with the following struct:

import Foundation
import SwiftyJSON

struct Pokemon {
    let name: String
    var types: [String?]? = []
    let spriteUrl: String?
    
    init(json: JSON) throws {
        name = json["name"].stringValue.capitalizedString
        
        types?.append(json["types"][1]["type"]["name"].string)
        types?.append(json["types"][0]["type"]["name"].string)
        
        spriteUrl = json["sprites","front_default"].string?.stringByReplacingOccurrencesOfString("http", withString: "https")
    }
}

The initializer takes in JSON as a parameter and sets the Pokémon’s properties using SwiftyJSON. Note that the types and spriteUrl properties are optionals. This is because they might not exist for a given Pokémon.

Let’s retrieve the Pokémon JSON using another Siesta Resource. Head back to Pokeapi.swift and add the following configureTransformer code to the init() function:

self.configureTransformer("/pokemon/*") {
    try Pokemon(json: $0.content)
}

This transformer will take any resource that accesses a Pokémon (i.e. the /pokemon/* endpoint of the API where * is the id of the Pokémon) and pass the JSON to the Pokemon struct’s initializer. This means we can create a Resource that returns an instance of a Pokemon struct. Add the code to do just that to the bottom of the _Pokeapi class:

var pokedex: Resource { return resource("/pokemon").withParam("limit", "151") }

func pokemon(id: String) -> Resource {
    return pokedex.child(id)
}

This code allows us to call Pokeapi.pokemon("1") to get a Resource for the Pokémon with id=1. Let’s use that to load the details page with our Pokémon data.

Gotta View ‘em All

Open up PokemonViewController.swift and change the class declaration to add the ResourceObserver property:

class PokemonViewController: UIViewController, ResourceObserver {

Next, add the StatusOverlay just like we did for PokedexViewController:

var statusOverlay = ResourceStatusOverlay()

override func viewDidLayoutSubviews() {
    statusOverlay.positionToCoverParent()
}

override func viewDidLoad() {
    super.viewDidLoad()
    statusOverlay.embedIn(self)
}

Now we’ll use the Resource we created to load up the Pokémon data. Add this code just below the IBOutlets at the top of the file:

var pokemonResource: Resource? {
    didSet {
        oldValue?.removeObservers(ownedBy: self)
    
        pokemonResource?.addObserver(self)
            .addObserver(statusOverlay, owner: self)
            .loadIfNeeded()
    }
}

func resourceChanged(resource: Resource, event: ResourceEvent) {
    showPokemon()
}

var pokemon: Pokemon? {
    return pokemonResource?.typedContent()
}

func showPokemon() {
    if let _pokemon = pokemon {
        nameLabel?.text = _pokemon.name.capitalizedString
        imageView?.imageURL = _pokemon.spriteUrl
        firstTypeLabel?.text = _pokemon.types?[0]
        secondTypeLabel?.text = _pokemon.types?[1]
    }
}

This Pokémon Resource works just like the Pokedex list did with the added nicety that we now have access to a Pokemon model. When the resourceChanged function is called the showPokemon() function checks to see if pokemon is not nil. If it’s not nil the _pokemon variable is used to populate the UI for the Pokémon. Add a call to showPokemon() in viewDidLoad:


override func viewDidLoad() {
    super.viewDidLoad()
    statusOverlay.embedIn(self)
    
    showPokemon()
}

All that’s left to do is to show PokemonViewController when a Pokémon is tapped in PokedexViewController. Replace the prepareForSegue function in PokedexViewController with the following code:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "showDetail" {
        if let pokemonVC = segue.destinationViewController as? PokemonViewController,
            let indexPath = self.tableView.indexPathForSelectedRow {
            pokemonVC.pokemonResource = Pokeapi.pokemon("\(indexPath.row + 1)")
        }
    }
}

This code sets the pokemonResource property on the destination PokemonViewController with a call to Pokeapi.pokemon(id) passing in the id for the tapped Pokémon.

Run the app and tap on a Pokémon. You should see the familiar loading indicator and then you’ll see the Pokémon’s details load like this:


The first time a Pokémon loads it is fetched from the API but if you tap on it again it will load immediately since Siesta caches the value in memory. That’s it, you built a mini-Pokédex using Pokeapi and the Siesta framework. Now go out there and catch ‘em all!

What’s Next?

With the aid of SwiftyJSON and Siesta we were able to create an API-driven Pokédex based using Pokeapi.co. Siesta removes a lot of boilerplate networking code and focuses on managing the resources in the API you’re working with. This means we were able to focus mostly on app logic and way less on writing network code.

I hope you’re excited to try more things with the Siesta framework. Here are some ways you could improve on this Pokédex:

  • Add more Pokémon attributes to the details page
  • Add sprite images to the Pokédex list
  • Write the Pokémon to disk using a persistent cache so that it works offline

Let me know what you decide to build. You can find me on Twitter @brentschooley or email me at brent@twilio.com. Happy Pokémon hunting!