[Swift] How to Asynchronously Download and Cache Images without Relying on Third-Party Libraries

Update October 2016: Fully updated for Xcode 8 and Swift 3

Loading a massive number of images asynchronously in a scrollable view like UITableView can be a common task. However, keeping the app responsive on scrolling while images are being downloaded can be a bit of challenge.

Many developers rely on libraries like Alamofire and SDWebImage to avoid the background image loading eventual troubles and cache management hassle. Something we did a while ago in this tutorial.

But what if you want to do all of that with your own pure code, without the help of third-party libraries ?

Well, that is what you gonna do here and now :]

In this tutorial, you are going to use your skills to build a smooth app that pull out a list of games titles and icons from the iTunes API. Along the way, you will understand how GCD (Grand Central Dispatch) and NSCache work together to manage networking images and produce a neat cache management system.

Without further ado, let’s build it!

Open up Xcode, select “File\New\Project” from the menu, choose the Single View Application template and name the project “ImagesDownloadAndCache”.

Create new Xcode project

Setting up the UI

The app you are about to build consists of a single interface, a table view with a refresh control to load the content.

The Xcode storyboard file comes with a default UIViewController, let’s remove it and replace with a UITableViewController instead, since it has a refresh control property by default.

Select Main.storyboard from the Project navigator view, drag a UITableViewController object from the Object library to the canvas. Next, select the initial View Controller and hit delete from the keyboard.

Note: Choose an iPhone model to work with along this tutorial, I selected the iPhone 6s model.

Choose a device model to layout with from the "View as" panel in Xcode 8

Select the recently added Table View Controller, then check the “Is Initial View Controller” from the Attributes inspector view.

Check Is Initial View Controller

Select ViewController.swift from the Project navigator view and change the class declaration so that it’s a UITableViewController subclass.

class ViewController: UITableViewController {

Switch back to the storyboard file, select the Table View Controller from the canvas then set the class to ViewController in the Identity inspector view.

Set the custom class for the View Controller in the Xcode 8 Identity inspector

Also, let’s add a navigation controller to the view hierarchy so that you can benefit from the navigation bar layout. To do so, make sure the View Controller is selected in the canvas then select “Editor\Embed In\Navigation Controller” from the menu.

As a last step, let’s assign an identifier for the custom cell so that you can reuse it from your code.

Select the Table View Cell from the canvas or from within the Document Outline view, then from the Attributes Inspector set the Identifier to “GameCell”.

Set an Identifier for a Custom Cell in Xcode 8

That’s it for the layout, now it’s time for some code!

Select the ViewController.swift file from the Project navigator view to load it in the editor.

To start off, copy the following properties declarations (just after the class name):

var refreshCtrl: UIRefreshControl!
var tableData:[AnyObject]!
var task: URLSessionDownloadTask!
var session: URLSession!
var cache:NSCache!

The data will be downloaded using the URLSessionDownloadTask class, that explains why you declared the task and session properties above. Also, the tableData property will serve as the table view data source and the cache variable is a reference for the cache dictionary the app will use to request cached images -if they exist- before downloading them.

Next, locate the viewDidLoad method and implement the following code just after the super.viewDidLoad call:

session = URLSession.shared
task = URLSessionDownloadTask()
        
self.refreshCtrl = UIRefreshControl()
self.refreshCtrl.addTarget(self, action: #selector(ViewController.refreshTableView), for: .valueChanged)
self.refreshControl = self.refreshCtrl
        
self.tableData = []
self.cache = NSCache()

The code above will initialise the session task, the table view data source, as well as the cache object. It will also attach a selector to the refresh control to run each time you pull the table view. Let’s go ahead and implement the “refreshTableView” selector.

func refreshTableView(){
        
let url:URL! = URL(string: "https://itunes.apple.com/search?term=flappy&entity=software")
task = session.downloadTask(with: url, completionHandler: { (location: URL?, response: URLResponse?, error: Error?) -> Void in
            
       if location != nil{
            let data:Data! = try? Data(contentsOf: location!)
            do{
                let dic = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves) as AnyObject
                self.tableData = dic.value(forKey : "results") as? [AnyObject]
                DispatchQueue.main.async(execute: { () -> Void in
                    self.tableView.reloadData()
                    self.refreshControl?.endRefreshing()
                })
            }catch{
                print("something went wrong, try again")
            }
       }
})
  task.resume()
}

Basically, the method above will asynchronously request the iTunes search API to return games containing the term “flappy” in their titles or descriptions. Once the data are received, the tableData data source array is filled in with records and the table view is reloaded from the main thread.

To finish off, you will implement two mandatory data source protocol methods to populate the downloaded data, especially images, in the table view rows.

Copy the following code before the class closing bracket:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.tableData.count
}    
    
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
// 1
let cell = tableView.dequeueReusableCell(withIdentifier: "GameCell", for: indexPath)
let dictionary = self.tableData[(indexPath as NSIndexPath).row] as! [String:AnyObject]
cell.textLabel!.text = dictionary["trackName"] as? String
cell.imageView?.image = UIImage(named: "placeholder")
        
if (self.cache.object(forKey: (indexPath as NSIndexPath).row as AnyObject) != nil){
    // 2
    // Use cache
    print("Cached image used, no need to download it")
    cell.imageView?.image = self.cache.object(forKey: (indexPath as NSIndexPath).row as AnyObject) as? UIImage
}else{
    // 3
    let artworkUrl = dictionary["artworkUrl100"] as! String
    let url:URL! = URL(string: artworkUrl)
    task = session.downloadTask(with: url, completionHandler: { (location, response, error) -> Void in
    if let data = try? Data(contentsOf: url){
    // 4
    DispatchQueue.main.async(execute: { () -> Void in
    // 5
    // Before we assign the image, check whether the current cell is visible
    if let updateCell = tableView.cellForRow(at: indexPath) {
    let img:UIImage! = UIImage(data: data)
    updateCell.imageView?.image = img
    self.cache.setObject(img, forKey: (indexPath as NSIndexPath).row as AnyObject)
}
})
}
})
task.resume()
}
return cell
}

You just implemented the main part of the app, let me breakdown the code above for better understanding:

// 1: Here, the table view will dequeue a cell for reuse. If no cell is allocated, then the table view will allocate, resize and return a new cell. Next, the current game record in the data source array is extracted in a dictionary object. The game title is set to the cell text label and the cell image is temporary assigned to a placeholder image while waiting for it to download.

// 2: The cache object is a collection-like container, very similar to an NSDictionary instance. Here you used it as a collection of UIImage objects, where the key is the row index (this is very important in order to keep track of the right cache image corresponding to each table cell). So basically, you first check whether there is a cached copy of the current image. If a copy already exists, then you load it in the cell image view.

// 3: If there is no cached copy of the given image, then you asynchronously download it from server.

// 4: Assuming the image is downloaded successfully, the code will switch to the main thread in order to load it to the image view. This is important since all UI tasks should be performed in the main queue and not in a background thread.

// 5: This is the tricky part, it’s important to check whether the cell is visible on screen before updating the image. Otherwise, the image will be reused on each cell while scrolling. The check is a performance saver and mandatory to implement. If the concerned cell is visible, then you just assign the image to cell image view and add it to the cache collection for later use.

You are done with the code. But before running the app, download the placeholder image here and import it to the Xcode project (drag and drop).

Also, disable the ATS (Application Transport Security) for the app in order to be able to perform networking operations. To disable the ATS, select the Info.plist file from the Project navigator view and change it to the following:

Disable App Transport Security

That’s it, run the app, pull to refresh to load the images and enjoy your work :]

As usual, you can download the final project here. Feel free to leave me a comment below, I’d like to hear from you!


Discover more from SweetTutos

Subscribe to get the latest posts sent to your email.

Malek
Software craftsman with extensive experience in iOS and web development.