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 6.3, as of April 16, 2015

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.

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

var tableData = []

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’m always open to questions and further discussion in the comments though, so feel free to chime in!

func searchItunesFor(searchTerm: String) {
    // The iTunes API wants multiple terms separated by + symbols, so replace spaces with + signs
    let itunesSearchTerm = searchTerm.stringByReplacingOccurrencesOfString(" ", withString: "+", options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil)
    
    // Now escape anything else that isn't URL-friendly
    if let escapedSearchTerm = itunesSearchTerm.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding) {
        let urlPath = "http://itunes.apple.com/search?term=\(escapedSearchTerm)&media=software"
        let url = NSURL(string: urlPath)
        let session = NSURLSession.sharedSession()
        let task = session.dataTaskWithURL(url!, completionHandler: {data, response, error -> Void in
            println("Task completed")
            if(error != nil) {
                // If there is an error in the web request, print it to the console
                println(error.localizedDescription)
            }
            var err: NSError?
            if let jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &err) as? NSDictionary {
                if(err != nil) {
                    // If there is an error parsing JSON, print it to the console
                    println("JSON Error \(err!.localizedDescription)")
                }
                if let results: NSArray = jsonResult["results"] as? NSArray {
                    dispatch_async(dispatch_get_main_queue(), {
                        self.tableData = results
                        self.appsTableView!.reloadData()
                    })
                }
            }
        })
        
        // The task is just an object with all these properties set
        // In order to actually make the web request, we need to "resume"
        task.resume()
    }
}

Let’s go line-by-line.

First, we need to do some fixing of the search terms we pass in, the Search API wants terms in the form of “First+Second+Third+Words” rather than “First%20Second%20…” etc. So instead of URL-encoding, we use an NSString method called stringByReplacingOccurencesOfString. This returns a modified versions of the searchTerm variable with all our spaces replaced with + symbols.

Next, we actually escape the search term in case there are other symbols that won’t fit in a URL by using the function stringByAddingPercentEscapesUsingEncoding.

The next 2 lines define an NSURL object that can be used as a Request URL for iOS’s networking API.

These next two lines are what gets us going with a NSURLSession and defines the task we want it to perform. This is were the heavy lifting begins, as the dataTaskWithURL method takes a closure as it’s final argument, which is executed after a request is sent, and a result is determined.

let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url, completionHandler: {data, response, error -> Void in

The first line grabs the default NSURLSession object. This is used for all our networking calls. The second line then creates the connection task which is going to be used to actually send the request. dataTaskWithURL has a closure as it’s last parameter, which gets run upon completion of the request. Here we check for errors in the response, then parse the JSON, and call the delegate method didReceiveAPIResults.

Finally, task.resume() actually begins the request.

Because this task happens in the background, we need to jump in to the foreground before we update the UI. So we need to use dispatch_async to move back in to the main thread, and reload the table view. The first argument specifies the thread the update should perform on, which we specify as dispatch_get_main_queue(). This is a built-in method that simply returns the main thread (the UI thread).

Making the API call

Now we’ve got a method that starts an iTunes search response when we call it. So let’s insert the following at the end of viewDidLoad…

searchItunesFor("JQ Software")

This will find any software products on the iTunes store containing that phrase, which in this case will include a couple of games and various apps.

Receiving the response

Finally our request code is done and all data has been received, didReceiveAPIResults is called and we’re ready to use the data in our app. Hooray!

The closure method here uses the NSJSONSerialization class to convert our raw data in to useful Dictionary objects by deserializing the results from iTunes.

We can now set our self.tableData object to be the resulting data, and tell the appsTableView to reload it’s content. This will cause the Table View object to run it’s own delegate methods. Defining this is the final step in this part of the tutorial.

Updating the Table View UI

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:

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

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "MyTestCell")
    
    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,
        // Create an NSURL instance from the String URL we get from the API
        imgURL = NSURL(string: urlString),
        // Get the formatted price string for display in the subtitle
        formattedPrice = rowData["formattedPrice"] as? String,
        // Download an NSData representation of the image at the URL
        imgData = NSData(contentsOfURL: imgURL),
        // 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 imageView cell to use the downloaded image data
            cell.imageView?.image = UIImage(data: imgData)
            // Update the textLabel text to use the trackName from the API
            cell.textLabel?.text = trackName
    }
    return cell
}

The numberOfRowsInSection is now simply returning the number of resultant objects from the tableData member, which is set in our prior connectionDidFinishLoading method.

The cellForRowAtIndexPath is also not changed dramatically in this case. Rather than simply returning the row number, we use the row number to grab three pieces of information: the track name, the artwork url, and the price.

Using these keys we construct the title, subtitle, and an image to go along with the cell.

If you’re coming from another language, you may notice the syntax is a little unusual here. The syntax goes something like this:

if let variableA = optionalThing as? Type,
    variableB = anotherOptionalThing as? Type {
        // variableA and variableB are confirmed to exist
}

This is part of optional binding. The statement “if let variableA = optionalThing as? Type {}” will only execute the code within {} if optionalThing is able to be casted to Type. It’s a check.. can we assign a variable called variableA to optionalThing as a type of Type? If so, then execute this next bit of code. You can also specify multiple checks, in this case we check if both variableA and variableB are able to be assigned, if so then we execute the code in the block.

We use optional binding in our cellForRowAtIndexPath method in order to make sure artworkUrl60, formattedPrice, and trackName are actually keys we get back from the API. If any of these fail, the code within the curly braces is skipped, preventing an error from occurring when we try to set the text or image to a nil value.

 

Try running our app, and you’ll see we for the first time have created something that actually looks like an app all in Swift!

But, why is it so laggy!?
If you scroll around you might notice it lags when showing table view cells.
In this tableview, we’re not properly handling quite a few things. Over the next 3 sections we’ll dive in to what that means and what the proper changes we need to make are. If you like, jump ahead to part 5 where these issues are resolved.

In my upcoming book, I delve more deeply in to why this happens. Learn more about it here.

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!

The completed source code to this section is available here.

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