[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!

Malek
iOS developer with over than 11 years of extensive experience working on several projects with different sized startups and corporates.

13 Comments

  1. Using indexPath.row as key to refer object in NSCache will create buga. What if table view dataSource changes? I think we will be referring to wrong image then.

  2. Hi Aashish 🙂
    If the data source is concerned for changes. Then I recommend using some tag types as keys in the cache collection to avoid keys duplicates. Something like incremental number.

  3. Hello. With your code if i close the app and run it again, it downloads the images again. So it saves it in cache only for when the app is open? This is no cache!

  4. Hi, it is cache 🙂
    NSCache has many policies on how it works and retain the right to discard cached data whenever needed to free up system memory.

    Each time the application is entered in background, the cache will be removed. So you can store the cache to NSUserDefaults when it goes to background and restore/reassign it from NSUserDefaults when the app becomes active.

    Quoted from NSCache class documentation:
    “An NSCache object is a collection-like container, or cache, that stores key-value pairs, similar to the NSDictionary class. Developers often incorporate caches to temporarily store objects with transient data that are expensive to create. Reusing these objects can provide performance benefits, because their values do not have to be recalculated. However, the objects are not critical to the application and can be discarded if memory is tight. If discarded, their values will have to be recomputed again when needed.

    While a key-value pair is in the cache, the cache maintains a strong reference to it. A common data type stored in NSCache objects is an object that implements the NSDiscardableContent protocol. Storing this type of object in a cache has benefits, because its content can be discarded when it is not needed anymore, thus saving memory. By default, NSDiscardableContent objects in the cache are automatically removed from the cache if their content is discarded, although this automatic removal policy can be changed. If an NSDiscardableContent object is put into the cache, the cache calls discardContentIfPossible on it upon its removal.”
    More details: https://developer.apple.com/library/ios/documentation/Cocoa/Reference/NSCache_Class/

  5. Hello, How can I cache videos downloaded from network? Also, how can I maintain the cache when the app is in foreground and the user goes to a different view and comes back to the view with images/videos? Thanks, Raj

  6. sorry for VERY late reply! forgot about Disqus!
    Although i learned many things about cache some days after the question to you, you are completely right!

Comments are closed.