Networking in Swift: Building the Swift Client, Part 4

This is part four in our tutorial series on building a Swift client application to query a RESTful API and serve the response in a list view to the end user of our app. When we left off in part three, we had successfully created a list view of the items in the two inventories supplied by the API. But a simple list of item names isn't very exciting. Let's add thumbnail images to display alongside the item names in our inventory listings.

The images we'll display alongside our inventory items are not stored within our Xcode app bundle, rather, if you recall, they are served up by our custom API along with item id's, names, and descriptions. We will therefore download the inventory item images and display them to the left of our item names. You can download the image assets from our Git repository for the project here, so as not to have to create mock image files with the appropriate file names yourself. After you've downloaded the files, place them in an appropriate folder alongside the other files for your inventory API.

Multithreading in Swift
All user interface related events, touches as well as view updates, occur on the main thread. Thus, if we perform operations that take a long time to complete, this will block or prevent the user interface from responding and or updating view elements—this is not desired behavior since it makes for a terrible user experience. To prevent blocking the user interface from receiving touch events while we download an image, we will download images on a secondary thread.

To get images we need their URLs. Recall that within our item model object (i.e. our PlumbingSupplyItem class) we have a property called 'bsn_image'. This property holds a URL to a unique image that corresponds to the item name in the same model object. 

In our PlumbingSupplyInventoryTableViewController class, we'll thus create an operation queue that will run on a thread other than the main thread. Toward the top of that class type the following line of code:

The string in the quotation marks here is completely arbitrary, and serves to avoid name collisions. In the function func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell, add the following snippet:

The first line of code (line 2) gets the image URL for the cell that is about to appear onto the screen. We will use this URL to download the corresponding image. The second line of code (line 4) is a call to the dispatch system. It copies the code that is nested within the closure and places this copy on our backgroundQueue. The code within the closure comprises an operation. Our backgroundQueue will then dispatch the copied block of code at a system determined time—in the immediate future. This is known as executing an operation. (The code above is structured in such a way that, for each instance of a cell that appears onto the screen, an operation is spawned and placed onto our operation queue. For example, when our table view first loads we have at most the same amount of operations as there are visible cells.) For our next line of code, currently just a comment, we want to capture the index of the cell that is requesting this image download operation. As a result, each operation on our queue will have a record of the cell's index that requested this operation. Now place the following line of code under that comment (on line 6):

For our next lines of code, currently just a comment, we retrieve an image's URL and begin downloading it. The NSData class has the following method that will synchronously download some data from the provided URL. (Synchronously, in this context, means that the request will block our thread, but not the main thread, until the download is complete. This is desired because we want the image's data to download before proceeding to the next lines of code that processes and displays the images). Add the following code below the relevant comment:

On the next line of code we verify that the image was downloaded without a hitch. If the image failed to download, then the error object would not be nil. Since displaying an image in an already displaying cell is considered a user interface update, it must occur on the main thread or else our images will not get displayed. To accomplish this we will dispatch operations to the main queue; on the next line of code, the call to the dispatch system does just that. For our next line of code, currently just a comment, we create an UIImage object from the raw downloaded data. Place this line of code under the relevant comment:

The following line of code gets the index of the cell that corresponds to the cell that spawned this image download operation. Place this line of code under the relevant comment:

Now locate the if-statement with the text, '/* compare indices */', and replace the comment with the following line of code:

This if-statement compares the captured cell index to the cell's current index. If the captured cell index is equal to the current cell index, then the cell that requested the image is still on the screen. We therefore present the downloaded image by executing the code that is nested within this if-statement.

When scrolling the table view,  the cells are reused to improve performance. When a cell is scrolled off the screen, instead of being purged from memory it is instead reused with a new index assigned to it. The issue is that when a cell is reused and subsequently redisplayed onto the screen, the cell spawns another image download operation which puts us in a state where we have more than one operation associated with any given cell.

To understand the issue here, suppose that the user scrolled the table view enough times that a single cell has four associated image download operations. When the individual operations complete their task, they message the cell by attempting to load an image into it. The visual result of four operations loading an image into one cell is a rapid succession of images changing as each operation loads its image. This behavior is not desired. Also, since the images may not download in the same order in which they were requested, we may wind up displaying the wrong image—this is bad!

The next two lines of code set the image on the current cell and tells the cell to lay out its cell for the newly provided image. Run the app and select one of the two plumbing supply categories. You should now see a table view with images and associated text as depicted below:
Success! We've displayed the relevant thumbnail images alongside the names of the items in our inventory lists. We're almost done! In the next and final article in the series, we will construct our individual item detail views. But before we conclude, let's return to the image-display issue mentioned above. 

To see the effect of allowing each operation to set a cell's image by not checking cell indices do the following. Comment out the if-statement: if currentIndex?.item == capturedIndex!.item). Now locate this line of code: self.dataSource = self.inventoryItems(data). Just below that line, load the data source multiple items by adding this line of code: for index in 1...5 { self.dataSource += self.dataSource; }.

Run the app and select one of the two categories. On an inventory items list page, quickly scroll down to the last element, you should see the images changing in rapid succession! To fix this effect, uncomment the if statement code only. Run the application again and quickly scroll to the last element. The issue should have been fixed.

It's good practice to cache data that is expensive to download. In our example, every time we scroll to a cell with an image that has already been shown, we don't request it from a local memory cache but instead download it every time. Our accompanying project repository contains sample code with a cache implementation. Simply comment out existing code and uncomment 'Version Two' sample code for a caching implementation—located in two places. If you run this implementation you will notice that the images load much faster when scrolling. This is highly desired behavior. 

This project can be found on GitHub.

That's it for the present piece. In the final article in the series, we will construct our individual item detail views.

Introduction and Overview: From the Back End API to the End User Application

The Web API
Building a RESTful API in PHP, Part 1
Building a RESTful API in PHP, Part 2

The Swift Client App
Networking in Swift: Building the Swift Client, Part 1
Networking in Swift: Building the Swift Client, Part 2
Networking in Swift: Building the Swift Client, Part 3
Networking in Swift: Building the Swift Client, Part 4
Networking in Swift: Building the Swift Client, Part 5

This tutorial was authored by Stefan Agapie, a Senior iOS Software Engineer, and adapted for the present piece. 

No comments:

Post a Comment