Loading a large number of images asynchronously in scrollable views such as UITableView is a common task. However, keeping the app scrolling smoothly while images are being downloaded can be a bit of a challenge.

Many developers rely on libraries like Alamofire and SDWebImage to avoid the ultimate hassle of background image loading and cache management. But what if you want to use pure code yourself instead of relying on third-party libraries?

Well, here’s what I’m talking about:

In this article, you’ll use your skills to build a smooth application that extracts a list of game titles and ICONS from the iTunes API. Along the way, you’ll learn how GCD (Grand Central Dispatch) and NSCache work together to manage network images and produce a clean cache management system.

Ok, let’s get started:

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

# # # # to create the UI

The application you are about to build contains a single interface with a table view of refresh controls to load content.

Xcode Storyboard file has a default UIViewController, so let’s remove it and replace it with a UITableViewController, which by default has a refresh control property.

Select Main.storyboard from the project navigator view and drag the UITableViewController object from the object library to the canvas. Next, select the original View Controller and click Delete from the keyboard.

Note: Select an iPhone model, in this case I chose the iPhone 6S model.

Select ViewController.swift from the project navigator view and change the class declaration to make it a UITableViewController subclass.

class ViewController: UITableViewController {
Copy the code

Switch back to the storyboard, select TableViewController, and then set the class to ViewController in the Identity Inspector.

Then we set up the identifier

Now that the interface is set up, we can start coding

First, let’s do the property declaration

var refreshCtrl: UIRefreshControl!
var tableData:[AnyObject]!
var task: URLSessionDownloadTask!
var session: URLSession!
var cache:NSCache<AnyObject, AnyObject>!
Copy the code

The data will be downloaded using the URLSessionDownloadTask class, which explains why you declared the task and session properties above. In addition, the tableData property will be used as the table view data source, the cache variable is a reference to the cache dictionary, and the APP will use the cache dictionary to request the cached image (if it exists) before downloading the image.

Next, find the viewDidLoad method and execute the following code 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()
Copy the code

The above code initializes the session, Task, tableData, and cache cache objects. A refresh call method was also added to run each time the table view was extracted. Let’s continue implementing 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?) -> Voidin
            
       iflocation ! = 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 inself.tableView.reloadData() self.refreshControl? .endRefreshing() }) }catch{print("something went wrong, try again")
            }
       }
})
  task.resume()
}
Copy the code

Basically, the above method asynchronously requests the iTunes search API to return its title or description. Once the data is received, the tableData data source array is filled with records and the table view is reloaded from the main thread.

Two mandatory data source protocol methods are then implemented to populate the downloaded data in table view rows, especially image data.

Copy the following code into the class:

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)
  letdictionary = 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 cacheprint("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) {
                        letimg:UIImage! = UIImage(data: data) updateCell.imageView? .image = img self.cache.setObject(img,forKey: (indexPath as NSIndexPath).row as AnyObject)
                  }
              })
          }
      })
      task.resume()
  }
  return cell
}
Copy the code

Let me break down the above code to better understand the main parts of the application I just implemented:

1: Here, the tableView will list the cell for reuse. If no cell is allocated, tableVeiw allocates, resizes, and returns a new cell. Next, extract the current cached record from the data source array in the dictionary object. The game title is set to the cell’s text label, and the cell is temporarily assigned to a placeholder image while waiting for it to download.

2: The cache is a collection-like container, much like an NSDictionary instance. Here you use it as a collection of UIImage objects, where the key is the row index (which is very important to keep track of the correct cached image for each cell). So basically, you first check to see if you have a cached copy of the current image. If the copy already exists, load it into the cell. 3: If no cache copy is given, it is downloaded asynchronously from the server. 4: Assuming the image downloads successfully, the main thread will be switched to update the view. This is important because all UI tasks should be executed in the main thread, not in background threads. 5: This is the tricky part, checking that the cell is visible on the screen before updating the image. Otherwise, the image will be reused on each cell as it scrolls. If the associated cell is visible, simply assign the image to the cell and add it to the cache for later use.

In addition, disable the APP’s ATS (Application Transfer Security) so that network operations can be performed. To disable ATS, select the info.plist file from the project navigator view and change it to the following:

More open source at https://github.com/CNKCQ