Developing iOS Apps Using Swift Part 5 – Async image loading and caching

This section completely updated to reflect changes in Xcode 6.3, as of April 16, 2015

In parts 1 through 4 we went over some basics of Swift, and set up a simple example project that creates a Table View and a puts some API results from iTunes inside of them. If you haven’t read that yet, check out Part 1

This table is slow! Let’s speed it up.

So, we now have the functionality we’re looking for, but if you run this yourself you’ll see its super slow! The issue is that the images in these cells are downloading on the UI thread, one at a time, and they aren’t being cached at all. So let’s fix that.

Let’s start by adding a lookup dictionary as a member for our SearchResultsViewController class:

var imageCache = [String:UIImage]()
Dictionary syntax
This is the first time we’ve seen this syntax so let me explain real quick.
The type specified here is [String : UIImage], this is similar to the Objective-C NSDictionary type, a HashMap in Java, or just Hash in Ruby/JS, but in Swift it’s very strict about the type. It takes a String as a key, and stores a UIImage as a value; no other types will be accepted.

So for example, if I have an image named “Bob” set to the UIImage with the file name “BobsPicture.jpg”, I might add him to the dictionary like this:

imageCache["Bob"] = UIImage(named: "BobsPicture.jpg")

The UIImage(named: “Bob.jpg”) part is just to get a UIImage from the file named Bob.jpg, this is standard Swift syntax for local files, the dictionary uses a subscript to set or retrieve it’s values. So if I wanted to get that image of Bob back out, I could just use:

let imageOfBob = imageCache["Bob"]

The reason we add the two parentheses is to call the constructor to init the empty dictionary.
Just like how we use APIController(), we need to use [String : UIImage]().

Hope that makes sense, if not yell at me on Twitter about it.

Now, in our cellForRowAtIndexPath method, we want to do quite a few things, we basically want to access the images in our cache, or download them if they don’t exist while on a background thread. After a new image is downloaded, it should be stored in to the cache so we can access it the next time the image is needed, without needing to go start another download process.

Let’s start by moving the imgData call out of our optional chain, and instead put it inside of the block that updates the cells. We’ll do this so that we can use image data from our cache *or* perform a new download, depending on the situation. Here, we’re switching to using NSURLConnection’s sendAsynchronousRequest method in order to download the image data on a background thread.

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier) as! UITableViewCell
    
    if let rowData: NSDictionary = self.tableData[indexPath.row] as? NSDictionary,
        // Grab the artworkUrl60 key to get an image URL for the app's thumbnail
        urlString = rowData["artworkUrl60"] as? String,
        imgURL = NSURL(string: urlString),
        // Get the formatted price string for display in the subtitle
        formattedPrice = rowData["formattedPrice"] as? String,
        // Get the track name
        trackName = rowData["trackName"] as? String {
            // Get the formatted price string for display in the subtitle
            cell.detailTextLabel?.text = formattedPrice
            // Update the textLabel text to use the trackName from the API
            cell.textLabel?.text = trackName
            
            // Start by setting the cell's image to a static file
            // Without this, we will end up without an image view!
            cell.imageView?.image = UIImage(named: "Blank52")
            
            // If this image is already cached, don't re-download
            if let img = imageCache[urlString] {
                cell.imageView?.image = img
            }
            else {
                // The image isn't cached, download the img data
                // We should perform this in a background thread
                let request: NSURLRequest = NSURLRequest(URL: imgURL)
                let mainQueue = NSOperationQueue.mainQueue()
                NSURLConnection.sendAsynchronousRequest(request, queue: mainQueue, completionHandler: { (response, data, error) -> Void in
                    if error == nil {
                        // Convert the downloaded data in to a UIImage object
                        let image = UIImage(data: data)
                        // Store the image in to our cache
                        self.imageCache[urlString] = image
                        // Update the cell
                        dispatch_async(dispatch_get_main_queue(), {
                            if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
                                cellToUpdate.imageView?.image = image
                            }
                        })
                    }
                    else {
                        println("Error: \(error.localizedDescription)")
                    }
                })
            }
            
    }
    return cell
}

So what’s all this code mean? Let’s run through the changes real quick…

IMPORTANT!

Before we download the real image we set the cell’s placeholder image. This is required if you want the cell to actually include an image view. Otherwise even loading in our image later will not show up! Create a blank image (I’m using 52×52 pixels, but it doesnt matter much) and Import it in to your project by click+dragging a file from finder in to you Xcode project, name it Blank52, and then set our cell to use this image. You can just grab my image file here (right-click and save as…)

cell.imageView?.image = UIImage(named: "Blank52")

Now our app should be less prone to crashing, and will now always have an image cell.

Put Image Downloading On A Background Thread

Now we need to check our image cache to see if this image has been downloaded before. We use the optional binding to check for the existence of our image in the cache:

if let img = imageCache[urlString] {
    cell.imageView?.image = img
}

If the image doesn’t exist (and initially it won’t) we need to download it. There are a couple of ways to initiate a download. Previously we used NSData’s dataWithContentsOfFile, but here we’re going to switch to NSURLConnection’s sendAsynchronousRequest, more similar to how our API works. The reason being is that we want to send off lots of small requests for images real quick, and we want to do it in the background. So let’s do that.

Look at the line with a call to NSURLConnection’s static method sendAsynchronousRequest, which takes a function/closure as a parameter for completionHandler. The lines after this call represent a function that is executed only *after* the async request returns.

NSURLConnection.sendAsynchronousRequest(request, queue: mainQueue, completionHandler: { (response, data, error) -> Void in
    if error == nil {
        // Convert the downloaded data in to a UIImage object
        let image = UIImage(data: data)
        // Store the image in to our cache
        self.imageCache[urlString] = image
        // Update the cell
        dispatch_async(dispatch_get_main_queue(), {
            if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
                cellToUpdate.imageView?.image = image
            }
        })
    }
    else {
        println("Error: \(error.localizedDescription)")
    }
})

Inside the block we will get back a few variables: response, data, and error.

If an error exists, proceed to create a UIImage from the data using the UIImage(data: data) constructor.

Next, we set the image cache to save our fancy new image with a key of the image URL. Using the URL as the key means we can find the image in our dictionary any time it pops up, even in a completely different context.

self.imageCache[urlString] = image

Finally, we set the cell image, on the UI thread (main queue):

dispatch_async(dispatch_get_main_queue(), {
    if let cellToUpdate = tableView.cellForRowAtIndexPath(indexPath) {
        cellToUpdate.imageView?.image = image
    }
})

You’ll notice we’re also using the cellForRowAtIndexPath() method here. The reason we use this is because sometime’s the cell that this code was running for may no longer be visible, and will have been re-used. So, to avoid setting an image on an unintended cell, we retrieve the cell from the tableView, based on the index path. If this comes back nil, then we know the cell is no longer visible and can skip the update.

Okay! Give the project a run and see our amazing new blazingly fast, silky smooth table view!

The complete code up to this point is available on Github as a branch ‘Part5‘.

To get updates on this tutorial in your email, subscribe here. I’m also working on a book filled with practical tips and tutorials on working with Swift to develop iOS 8 apps. It’s now available for pre-order here.

Part 6 will focus on adding a new View Controller that we can open up to, loading it with iTunes data.

Go to part 6 now ->


Sign up now and get a set of FREE video tutorials on writing iOS apps coming soon.

Subscribe via RSS

Developing iOS Apps Using Swift Tutorial Part 2

This section completely updated to reflect changes in Xcode 8.3.1, as of April 17, 2017

In part 1 we went over some basics of Swift, and set up a simple example project that creates a Table View and a puts some text inside of them. If you haven’t read that yet, give it a read here.

For this section, we’re going to do something a little more ambitious. We’re going to hit the iTunes Search API for the iTunes Store, download the JSON results, parse them in to Dictionaries and then populate our Table View with this information. Then, we will add some user interaction by adding a click event to the tableview, so that when an item is clicked the iTunes store item will be opened.

If this sounds like a lot of work, don’t sweat it. This is pretty basic functionality for iOS apps and it’s one of the most common things any developer has to do. Let’s get going…

Connecting the UI

The first thing we need to do is get a reference to our tableView, so it can be used from code. Go ahead and add this line to your ViewController.swift file, just under the class definition, but outside of any functions.

@IBOutlet var appsTableView : UITableView!

This bit of code allows up to connect our Table View in our Storyboard to this variable, “appsTableView”. Save this file and open up your storyboard. Now select the View Controller object (the one with a yellow icon) and in the right-hand side pane click the last tab, the Connections Inspector. Here you should now see an outlet for “appsTableView”. Click and drag from the dot next to this outlet on to the Table View in our scene.

Connecting the Table View to the View Controller

Let’s also add a variable to hold the table data itself. Just under the class definition for ViewController add:

var tableData = [[String: String]]()

This variable is an Array type that contains multiple Dictionary types (or hashable types if you prefer). Inside these values the key is of type String as well as the value. Or in other words I can get or set String values by accessing this variable with any String key in the dictionary, as we’ll see in a moment. If that does’t make sense just yet, just keep going and you’ll see how it’s used later in the tutorial.

Making the API Request

Now that we have the UI connected, we’re ready to make an API call. Create a new function called searchItunesFor(searchTerm: String). We’ll use this to make our requests happen for arbitrary search terms.

To keep this tutorial short, I’m going to just post my final code and let the comments do some of the explaining. I’ll also break it down line-by-line afterward. Also, I’m always open to questions and further discussion in the comments though, so feel free to chime in!

func searchItunes(searchTerm: String) {
    // The iTunes API wants multiple terms separated by + symbols, so replace spaces with + signs
    let itunesSearchTerm = searchTerm.replacingOccurrences(of: " ", with: "+", options: .caseInsensitive, range: nil)
    // Also replace every character with a percent encoding
    let escapedSearchTerm = itunesSearchTerm.addingPercentEncoding(withAllowedCharacters: [])!
 
    // This is the URL that Apple offers for their search API
    let urlString = "http://itunes.apple.com/search?term=\(escapedSearchTerm)&media=software"
    let url = URL(string: urlString)!
    URLSession.shared.dataTask(with: url) { (data, response, error) in
        if let error = error {
            // If there is an error in the web request, print it to the console
            print(error)
            return
        }
        // Because we'll be calling a function that can throw an error
        // we need to wrap the attempt inside of a do { } block
        // and catch any error that comes back inside the catch block
        do {
            let jsonResult = try JSONSerialization.jsonObject(with: data!, options: []) as! [String: Any]
            self.didReceive(searchResult: jsonResult)
        }
        catch let err {
            print(err.localizedDescription)
        }
        // Close the dataTask block, and call resume() in order to make it run immediately
    }.resume()
}

First, we fix some issues with the search terms. For one thing, the iTunes API is not going to like seing spaces, instead it wants plus signs (+), so the replacingOccurences function does this work, and we put the result in the itunesSearchTerm variable.

Next, we also want to make sure no other characters will cause issues. For example a question mark (?) may be viewed by the API as the end of the request if it was included in the search term. So we use the method addingPercentEncoding to encoded every character in to a percent-encoded form, and store this new value in to escapedSearchTerm.

After that we define our URL, which is just the iTunes search API URL with the escapedSearchTerm inserted in to the term field. We store this in the variable urlString. Next, we convert this URL String in to a proper URL object. This will basically validate that the URL is valid, or at least properly-formed, and then it gives it some useful methods as well that are common to URLs.

Now comes the network request. We make the request from the URLSession API that Apple provides. You could create a URLSession for this task, but Apple’s API will provide a pre-constructed one called shared. We access this with the simple dot-notation URLSession.shared.

From here, we use the shared sesion to create a new Data Task by calling it’s dataTask function with the url we created the line before. The next part is the completion handler. This itself is a function we define inline, inside of the curly braces.

So, everything here inside of the curly braces, where the indentation is increased, is only executed after the Data Task has been completed. Hopefully when this happens, what we get back from the server is JSON data containing the search results! It’s also possible that there was some kind of error, so we check whether or not an error is present by using the optional binding syntax:

if let error = error {
...

This syntax replaces the optional error: Error? with an unwrapped optional error: Error, but only if it exists. Most of the time, the value of error will be nil, and this block of code will be skipped entirely.

Next we are going to decode the result from a Data object in to a dictionary we can use to access each JSON element in the results.

Because the Apple APIs for JSON deserialization can throw an error, we perform this work inside of a do { } block, with a catch let err block after it. If something goes wrong during JSON deserialization, the catch block will be executed. But if everything goes to plan, the do block will complete executing.

Once we get the deserialized data back, we call a new method self.didReceive(searchResult: jsonResult).
This method is not defined yet, so we’ll define it next.

Finally we call resume() at the end of the dataTask block in order to execute the API request immediately.

Getting the data we need out of the JSON response

Now that we are getting a JSON response back, we will want to take out the values we care about. In this case that’s the price, a thumbnail, and the name of the app. So next, let’s create the didReceive method inside our ViewController class in order to parse out this data and store it inside the tableData variable that will inform our Table View.

func didReceive(searchResult: [String: Any]) {
    // Make sure the results are in the expected format of [String: Any]
    guard let results = searchResult["results"] as? [[String: Any]] else {
        print("Could not process search results...")
        return
    }
 
    // Create a temporary place to add the new list of app details to
    var apps = [[String: String]]()
 
    // Loop through all the results...
    for result in results {
        // Check that we have String values for each key we care about
        if let thumbnailURLString = result["artworkUrl100"] as? String,
            let appName = result["trackName"] as? String,
            let price = result["formattedPrice"] as? String {
            // All three data points are valid, add the record to the list
            apps.append(
                [
                    "thumbnailURLString": thumbnailURLString,
                    "appName": appName,
                    "price": price
                ]
            )
        }
    }
    // Update our tableData variable as a way to store
    // the list of app's data we pulled out of the JSON
    tableData = apps
    // Refresh the table with the new data
    DispatchQueue.main.async {
        self.appsTableView.reloadData()
    }
}

Here we are first off checking that we have the right data type. The results argument passed in to the didReceive function here is our JSON value returned from the iTunes Search API. We are going to want to check for three keys:

  • artworkUrl100
  • trackName
  • formattedPrice

Why these keys? This is just the keys Apple chose in their API docs. If you take a look at a sample API response you’ll see keys contain the info we want about an app.

So what we do is we loop through everything inside of the array of results, and then check that each of these three values are present, and are convertible to a String type. Here we’re using a compound form of the optional binding syntax that allows us to check for the valid presence of multiple values. if let varA = valueA, let varB = valueB { }. Not only are we checking for their presence, but if they can be represented as String objects.

If all three keys are present, and they can all be represented as strings, then we have an app record! We’ll add it to the list of apps, a temporary variable we created to store each new app we come across for this query.

I’m using slightly different keys for our app’s purposes. This is mainly just to show there is no reason these keys must match.

Finally, once the list of apps is completely updated, we assign the tableData variable that is going to be used to hold the Table View’s data to the array of apps we just created. From there, we call reloadData() on the appsTableView, which let’s the table know there is new data to work with, and that it should re-render itself.

You’ll notice the reloadData() is inside of another block. This one is from the Grand Central Dispatch API. By putting this code inside of the block DispatchQueue.main.async { }, we’re able to assure this is executed on the foreground thread. Because this code is all being executed in response to a network request (the API call), we end up on a background thread. This is because we know we have to wait a moment for the API to respond. The iOS App won’t just freeze the app up while it waits for the response. Instead, it will send the response in to a background thread.

So, now that we are actually updating some UI, we want to jump back on to the main thread and make sure it shows up immediately. If you are curious, you can call reloadData outside of this block and see how it effects the performance of the app (after we update the actual UI in the next step)

Update the UI with the new data

Now that we’ve got our API call updating the tableData variable, and calling reloadData() on the appsTableView we connected earlier, we can proceed to actually implement those UITableViewDataSource methods we set up in the beginning of the tutorial…

You may remember from last time we implemented the function for our Table View that we had a count method, which determines the number of rows; and a cell method, which actually creates the cell and modifies it for each row.

We’re going to update these now to use the data we pulled down from the web.

Swap out your methods with these two functions:

// MARK: UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return tableData.count
}
 
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "MyTestCell")
 
    // Get the app from the list at this row's index
    let app = tableData[indexPath.row]
 
    // Pull out the relevant strings in the app record
    cell.textLabel?.text = app["appName"]
    cell.detailTextLabel?.text = app["price"]
 
    return cell
}

The first one is pretty simple. The number of rows should be the same as the count of the records in tableData (the number of app returned from the API).

The second function is what creates the cell, which is what is displayed in the Table View. First we call the constructor to make a fresh UITableViewCell to work with. Then we retrieve the current app that should be displayed by accessing tableData[indexPath.row]. The variable indexPath is passed in as an argument by Apple’s Table View class. It represents the current index that it is asking for a cell for. It’s kind of a backwards way of thinking about it, but basically this function is the table itself asking for an individual row. Which row? Well… the one at indexPath.row.

Next, we’ll set the text values for the text and detail labels of the cell. We’ll set these to be the value of the app array we created earlier in response to the API response.

Call the API

As a final step, we’ll manually make the API call for our searchItunes method by adding it inside of the viewDidLoad function. The viewDidLoad method is called on this View Controller object any time the screen it represents is being constructed. This is a common pattern in iOS development.

Find your viewDidLoad function (the Xcode template will have already added it) and add a call to the searchItunes function.

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
    searchItunes(searchTerm: "JQ Software")
}

It’s alive!

Finally, run the app! Do you see the search results? You should see a few of my apps from the App Store pop up if you used my JQ Software search term. Try some other search terms and rebuild the app to see the different results that come back. You’ll notice we don’t yet have images in place, but we’ll get to that in later sections. We did get the data for that already after all.

If you run the App using JQ Software as a search term it will look like this at this stage:

JQ Software Apps List

Next time in Part 3 we’ll work on the interaction piece of the app, allowing users to search for anything they like, and making the table cells clickable!

Download the Source Code

Go to the Next Part

Go to Part 3 now ->


Sign up now and get a set of FREE video tutorials on writing iOS apps coming soon.

Subscribe via RSS