Loading UITableViewCell Images From an API
March 23, 2015 - Updated: December 19, 2016 - Swift 3.0

Previously we set up a Swift app that:

  • Pulled Star Wars species data from the Star Wars API using Alamofire
  • Took the JSON from the response and turned it into an array of Swift objects
  • Parsed some string, array, and date fields in the web service JSON
  • Displayed the results in a table view
  • Loaded more results as the user scrolled down in the table view
  • Passed the table view to a detail view, using a storyboard & segue. Tapping on a row in the table view opened a detail view displaying additional data about that Star Wars species

Today we’ll add another feature: Showing images of each species in the table view. We’ll do that by getting the URLs from a web-based API then loading the images asynchronously from the URLs. We’ll have to handle table view cells getting reused while we’re trying to retrieve the images. Finally we’ll set up a cache so we don’t have to pull down the images every time a cell gets reused. The images will get pulled from DuckDuckGo’s Instant Answers API.

Here’s what it’ll look like when we’re done:
Screenshot of a tableview with cells showing star wars species

This tutorial has been updated to use Swift 3.0, Xcode 4, and Alamofire 4.1.

If you haven’t been following along, you might want to grab the code that’ll be our starting point from GitHub (Swift3.0_Detail_View branch).

Or if you’d rather not type, grab the completed code from this tutorial SwiftREST on GitHub (Swift3.0_API_Images branch).

Project Setup

Before we get into the gritty details we need to give credit where credit is due, i.e., we need to show attribution for the API and the images. We’ll need to deal with 2 types of attribution. First we need to tell people that we’re using the DuckDuckGo Instant Answers API. We’ll do that by displaying the attribution at the bottom of the main table view. Then we need to tell them where the images actually come from. We’ll do that by including the image source in each tableview cell.

For the API attribution, add a footer to the tableview in the storyboard:

  • Open the main storyboard and select the ViewController
  • Add a view below the cell in the tableview
  • Drop a label in the view
  • Set the label text: “All images from DuckDuckGo Instant Answers API”
Setting up attribution footer
Setting up attribution footer

The DuckDuckGo Instant Answers API

The DuckDuckGo Instant Answers API provides an endpoint to get info about a search query. Documentation is at https://api.duckduckgo.com/api. It’s not guaranteed to have an image but it usually does. We’ll have to be careful about that when parsing the JSON.

The results of a search like https://api.duckduckgo.com/?q=%22yoda%27s%20species%22&format=json look like (add &pretty=1 at the end to get nice formatting if you’re calling it in a web browser):

{
  "DefinitionSource" : "",
  "Heading" : "Yoda's species (Star Wars)",
  "ImageWidth" : 109,
  "RelatedTopics" : [
    {
      "Result" : "<a href=\"https://duckduckgo.com/2/c/Members_of_Yoda's_species\">Members of Yoda's species</a>",
      "Icon" : {
      "URL" : "",
      "Height" : "",
      "Width" : ""
    },
    "FirstURL" : "https://duckduckgo.com/2/c/Members_of_Yoda's_species",
    "Text" : "Members of Yoda's species"
    },
    {
      "Result" : "<a href=\"https://duckduckgo.com/2/c/Unidentified_sentient_species\">Unidentified sentient species</a>",
      "Icon" : {
      "URL" : "",
      "Height" : "",
      "Width" : ""
    },
    "FirstURL" : "https://duckduckgo.com/2/c/Unidentified_sentient_species",
    "Text" : "Unidentified sentient species"
    }
  ],
  "Entity" : "",
  "Type" : "A",
  "Redirect" : "",
  "DefinitionURL" : "",
  "AbstractURL" : "http://starwars.wikia.com/wiki/Yoda's_species",
  "Definition" : "",
  "AbstractSource" : "Wookieepedia",
  "Infobox" : "",
  "Image" : "https://duckduckgo.com/i/fd2dabcf.jpg",
  "ImageIsLogo" : 0,
  "Abstract" : "The Jedi Master Yoda was the best-known member of a species whose true name is not recorded. Known in some sources simply as Yoda's species, this species of small carnivorous humanoids produced several well-known members of the Jedi Order during the time of the Galactic Republic.",
  "AbstractText" : "The Jedi Master Yoda was the best-known member of a species whose true name is not recorded. Known in some sources simply as Yoda's species, this species of small carnivorous humanoids produced several well-known members of the Jedi Order during the time of the Galactic Republic.",
  "AnswerType" : "",
  "ImageHeight" : 200,
  "Results" : [],
  "Answer" : ""
}

We’re interested in 3 fields:

  • Image for the image URL
  • AbstractSource & AbstractURL for the source for attribution

Before figuring out how to parse those, let’s work out how we’ll want to make this call.

Calling the API

Add a new DuckDuckGoSearchController.swift file and import Alamofire:

import Foundation
import Alamofire

class DuckDuckGoSearchController {
}

We need to do 4 things when we make this call:

  1. Compose the search URL to hit based on the search string
  2. Fetch the JSON response for that URL and parse out the attribution & URL elements
  3. Fetch the image from the URL in those search results
  4. Return the attribution info and the image so we can display them

Before implementing the details for each of those tasks, let’s see how we’d like them to fit together. Assuming we create a class called ImageSearchResult to hold the attribution info, image URL, and image, we can do something like this:


class func image(for searchString: String, completionHandler: @escaping (Result) -> Void) {
  // Get the search URL
  guard let searchURLString = endpoint(for: searchString) else {
    completionHandler(.failure(BackendError.urlError(reason: "Could not create a valid search URL to get an image")))
    return
  }
  // Call the search URL
  Alamofire.request(searchURLString)
    .responseJSON { response in
      if let error = response.result.error {
        completionHandler(.failure(error))
        return
      }
      // Get the image URL & attribution info
      // Get the image
      DuckDuckGoSearchController.imageSearchResult(from: response) { imageSearchResult in
        // return the attribution info and the image
        completionHandler(imageSearchResult)
      }
  }
}

BackendError is an enum we set up in a previous tutorial:


enum BackendError: Error {
  case urlError(reason: String)
  case objectSerialization(reason: String)
}

That image(for searchString: completionHandler:) function executes the 4 steps that we listed above. It needs to use a completion handler to hand the result back to the caller because the networking calls are asynchronous. Other than a bit of error checking and making the Alamofire call to fetch the search results, most of the functionality is encapsulated in functions that we still need to write.

To make that code work, we’ll need to implement a few functions to handle those tasks:


endpoint(for: searchString)

To create the URL string from the string we want to search for.

And:


imageSearchResult(from: response)

To get the attribution info & image URL and then fetch the image.

And we need to create the ImageSearchResult class.

Search String Formatting

Add a function to your DuckDuckGoSearchController that will get the search endpoint for whatever search string you’re using:


class DuckDuckGoSearchController {
  // ...

  private class func endpoint(for searchString: String) -> String? {
    // URL encode it, e.g., "Yoda's Species" -> "Yoda%27s%20Species"
    // and add star wars to the search string so that we don't get random pictures of the Hutt valley or Droid phones
    guard let encoded = "\(searchString) star wars".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
      return nil
    }
    // create the search string
    // append &t=grokswift so DuckDuckGo knows who's using their services
    return "https://api.duckduckgo.com/?q=\(encoded)&format=json&t=grokswift"
  }
}

endpoint(for searchString:) takes in our search string (the species name from the SWAPI.co API call) and generates the URL that we need to request from DuckDuckGo. The input string has 2 modifications applied to it: it gets URL encoded (e.g., “Yoda’s Species” becomes “Yoda%27s%20Species”) and we add “star wars” so we don’t get random pictures of the Hutt valley or Droid phones.

Then we create the URL string by appending the query (&q) and the format we want returned (&format=json): https://api.duckduckgo.com/?q=\(encoded)&format=json&t=grokswift. It’s nice to let DuckDuckGo know who’s making the requests, so we tack that on too: &t=grokswift.

Parsing the Response

Like last time, we’ll set up a convenience function that takes the response to the Alamofire request and turns it into the object that we want. We’ll need a class that’ll hold all of the info for one of our images from search results, so let’s create a class for that. It’ll have those 3 properties and the actual image (after we retrieve it from the imageURL):


class ImageSearchResult {
  let imageURL: String
  let source: String?
  let attributionURL: String?
  var image: UIImage?
  
  required init(anImageURL: String, aSource: String?, anAttributionURL: String?) {
    imageURL = anImageURL
    source = aSource
    attributionURL = anAttributionURL
  }
}

And we’ll add a function to that class so we can get all of the attribution data as a single string, allowing for each of the bits to be missing. We’ll use that to display the attribution info in the table view cells:


class ImageSearchResult {
  // ...
  
  func fullAttribution() -> String {
    var result:String = ""
    if let attributionURL = attributionURL, !attributionURL.isEmpty {
      result += "Image from \(attributionURL)"
    }
    if let source = source, !source.isEmpty {
      if result.isEmpty {
        result += "Image from "
      }
      result += " \(source)"
    }
    return result
  }
}

Now, for that convenience function, we need to parse out the image URL as well as the 2 fields for attribution from the JSON:


class DuckDuckGoSearchController {
  static let IMAGE_KEY = "Image"
  static let SOURCE_KEY = "AbstractSource"
  static let ATTRIBUTION_KEY = "AbstractURL"
  
  // ...
  
  private class func imageSearchResult(from response: DataResponse, completionHandler: @escaping (Result) -> Void) {
    guard response.result.error == nil else {
      // got an error in getting the data, need to handle it
      print(response.result.error!)
      completionHandler(.failure(response.result.error!))
      return
    }
    
    // make sure we got JSON and it's a dictionary
    guard let json = response.result.value as? [String: Any] else {
      print("didn't get image search result as JSON from API")
      completionHandler(.failure(BackendError.objectSerialization(reason:
        "Did not get JSON dictionary in response")))
      return
    }
    
    guard let imageURL = json[IMAGE_KEY] as? String else {
      print("didn't get URL for image from search results")
      completionHandler(.failure(BackendError.objectSerialization(reason:
        "Did not get URL for image from search results")))
      return
    }
    let source = json[SOURCE_KEY] as? String
    let attribution = json[ATTRIBUTION_KEY] as? String
    let result = ImageSearchResult(anImageURL: imageURL, aSource: source, anAttributionURL: attribution)
    
    // TODO: get the image
    completionHandler(.success(result))
  }
}

First we check for errors from the networking call. Then we make sure the JSON is a dictionary. Then we check to see if we got an image URL in the results. If any of those checks fail, we kick out the error to the completion handler and return. If it succeeds then we extract the attribution info from the JSON then create an ImageSearchResult to store the URL & attribution info.

But that doesn’t include fetching the actual image to display. We’ll tackle that next.

Loading Images from URLs

So we’ve got everything set up to retrieve the image URLs and attribution data in imageSearchResult(from: response). Now we need to get the actual image data from that URL. We’ll finish off imageSearchResult(from: response) by implementing that functionality in place of // TODO: get the image above:


Alamofire.request(imageURL)
  .response { response in
    guard let imageData = response.data else {
      print("Could not get image from image URL returned in search results")
      completionHandler(.failure(BackendError.objectSerialization(reason:
        "Could not get image from image URL returned in search results")))
      return
    }
    result.image = UIImage(data: imageData)
    completionHandler(.success(result))
}

Since we need to fetch the image data from the URL we’re using an Alamofire.request. To get the actual data in the response instead of JSON, we hook up .response as a response handler instead of .responseJSON.

Within the response handler we don’t have to do much: just make sure we got data in the response (and kick out an error if not), and convert the data to an image. Finally, we add the image to the ImageSearchResult object that we created earlier (and that already has the attribution info):


result.image = UIImage(data: imageData)

And use the completion handler to return it.

UITableViewCell Images from URLs

Now we can hook up displaying images. We can call the image(for searchString: completionHandler:) function that we set up at the start of this tutorial since we’ve now implemented all of the bits required for it to work. In ViewController’s cellForRowAtIndexPath:


func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
  
  if let species = species, species.count >= indexPath.row {
    let speciesToShow = species[indexPath.row]
    cell.textLabel?.text = speciesToShow.name
    cell.detailTextLabel?.text = " "
    cell.detailTextLabel?.adjustsFontSizeToFitWidth = true
    cell.imageView?.image = nil
    if let name = speciesToShow.name {
      // this isn't ideal since it will keep running even if the cell scrolls off of the screen
      // if we had lots of cells we'd want to stop this process when the cell gets reused
      DuckDuckGoSearchController.image(for: name) {
        result in
        if let error = result.error {
          print(error)
        }
        let imageSearchResult = result.value
        // Save the image so we won't have to keep fetching it if they scroll
        if let cellToUpdate = self.tableview?.cellForRow(at: indexPath),
          cellToUpdate.imageView?.image == nil {
          cellToUpdate.imageView?.image = imageSearchResult?.image // will work fine even if image is nil
          cellToUpdate.detailTextLabel?.text = imageSearchResult?.fullAttribution()
          cellToUpdate.setNeedsLayout() // need to reload the view, which won't happen otherwise since this is in an async call
        }
      }
    }
    
    // See if we need to load more species
    // ...
  }
  
  return cell
}

Here’s the new bit:


if let name = speciesToShow.name {
  // this isn't ideal since it will keep running even if the cell scrolls off of the screen
  // if we had lots of cells we'd want to stop this process when the cell gets reused
  DuckDuckGoSearchController.image(for: name) {
    result in
    if let error = result.error {
      print(error)
    }
    let imageSearchResult = result.value
    // Save the image so we won't have to keep fetching it if they scroll
    if let cellToUpdate = self.tableview?.cellForRow(at: indexPath),
      cellToUpdate.imageView?.image == nil {
      cellToUpdate.imageView?.image = imageSearchResult?.image // will work fine even if image is nil
      cellToUpdate.detailTextLabel?.text = imageSearchResult?.fullAttribution()
      cellToUpdate.setNeedsLayout() // need to reload the view, which won't happen otherwise since this is in an async call
    }
  }
}

If we have a species name (which we should but it’s best to be paranoid, especially with data from web services), then we call DuckDuckGoSearchController.image(for: name). It’s an async call: when it’s done it’ll call our completion handler, so you won’t be able to just step through this code (toss an extra breakpoint in the completion handler if you want to step through it).

When we have an image, we need to set it in the cell’s UIImageView. But since we’re using tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) the tableview will reuse cells that have scrolled off of the screen. So if our cell has scrolled off of the screen then we shouldn’t set the image, since it’ll be for the wrong species.

We can work around this problem by using the indexPath: once we’ve got the image, ask the tableview for the cell for the indexPath (row & section): if let cellToUpdate = self.tableview?.cellForRow(at: indexPath)

If it has scrolled off of the screen and been recycled, then that’ll give us nil so we won’t set the image for the wrong cell.

If we do get the cell (i.e., it’s still on the screen) then we set the image & attribution text and tell the cell to redraw itself. Without cellToUpdate.setNeedsLayout() the cell wouldn’t get redrawn, so we wouldn’t see the image & attribution text until we scrolled the cell on & off of the screen or tapped on it. You don’t need to call cellToUpdate.setNeedsLayout() in this function if you’re not using asynchronous methods. But since we’re making some HTTP calls and we don’t want to hang up the UI while we’re waiting for the responses to those requests, we have to tell the cells to redraw when we’re done with them.

Enhancements

I’ve left a TODO in the code. We’re firing off requests to get the images for each cell but sometimes by the time we get the result we don’t need it anymore. An optimization would be to be cancel the Alamofire requests if the cell scrolls off of the screen. If you’re dealing with lots of images you’d want to do that (you’ll know you need it if your scrolling isn’t smooth). Since we only have 37 species and we’re going to add a cache, this concern isn’t one that needs to be addressed.

Another optimization that is worthwhile even for this small app is caching the images so we don’t have to grab them from the web every time we display them. We’ll do a quick & easy single-run-of-the-app cache today and build a better persistent cache in a future tutorial.

Caching Images

In our ViewController, we can add a dictionary to hold the images (actually, the whole ImageSearchResult including the attribution info). They’ll be indexed by the species names:


  var imageCache: [String: ImageSearchResult] = [:]

Now we can save the images & their attribution info when we get them by calling self.imageCache[name] = imageSearchResult in our cellForRowAtIndexPath function:


if let name = species.name {
  // this isn't ideal since it will keep running even if the cell scrolls off of the screen
  // if we had lots of cells we'd want to stop this process when the cell gets reused
  DuckDuckGoSearchController.imageFromSearchString(name, completionHandler: {
    (imageSearchResult, error) in
    if error != nil {
      print(error)
    }
    // Save the image so we won't have to keep fetching it if they scroll
    self.imageCache[name] = imageSearchResult
    if let cellToUpdate = self.tableview?.cellForRowAtIndexPath(indexPath)
    {
      if cellToUpdate.imageView?.image == nil
      {
        cellToUpdate.imageView?.image = imageSearchResult?.image // will work fine even if image is nil
        cellToUpdate.detailTextLabel?.text = imageSearchResult?.fullAttribution()
        cellToUpdate.setNeedsLayout() // need to reload the view, which won't happen otherwise since this is in an async call
      }
    }
  })
}

And then before retrieving the image, we’ll check the cache to see if we already have it:


if let cachedImageResult = imageCache[name] {
  cell.imageView?.image = cachedImageResult.image // will work fine even if image is nil
  cell.detailTextLabel?.text = cachedImageResult.fullAttribution()
} else {
  // load image from web
}

And That’s All

We’ve set up our Alamofire calls to retrieve an image URL from a search string, then to load the image from the URL. We then used those images in UITableViewCells, handling the asynchronous loading even though the cells might have been reused. We set up a primitive cache for a single run of the app that helps us avoid constantly reloading the images. In a future tutorial we’ll improve the cache so it persists between runs of the app.

If you’d like more Swift tutorials like this one, sign up below to get them sent directly to your inbox. Feel free to come back and visit too, we’re pretty friendly around here but we’ll understand if it’s just easier for us to come to you :)