Search This Blog

Sunday, January 28, 2018

iOS Swift. How to Asynchronously Download and Cache Images

Loading an Image Asynchronously


Download image from Internet using URLSession and its dataTask method. Image will be loaded asynchronously and set as image of  UIImageView in main UI thread using DispatchQueue.main.async.
import UIKit

class SingleImageViewController: UIViewController {
    
    @IBOutlet var imageView: UIImageView!
    
    let urlStr = "https://cdn.pixabay.com/photo/2017/05/07/08/56/pancakes-2291908_960_720.jpg"

    override func viewDidLoad() {
        super.viewDidLoad()
        
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        
        if let url = URL(string: urlStr) {
            let session = URLSession.shared
            let dataTask = session.dataTask(with: url) { (data, response, error) in
                if let unwrappedError = error {
                    print(unwrappedError)
                    return
                }
                
                if let unwrappedData = data {
                    let image = UIImage(data: unwrappedData)
                    DispatchQueue.main.async(execute: {
                        self.imageView.image = image
                    })
                }
                
            }
            dataTask.resume()
        }
    }

}

Display downloaded image




Make an Extension


In our app can be a lot of images that will be loaded from Internet. So we need some method which we can reuse everytime when we load image. Let's create Extension for UIImageView. It is always useful to make extension when task will be used in many places.
import UIKit

extension UIImageView {
    
    func loadImageWithUrl(urlString: String) {
        
        if let url = URL(string: urlString) {
            let session = URLSession.shared
            let dataTask = session.dataTask(with: url) { (data, response, error) in
                if let unwrappedError = error {
                    print(unwrappedError)
                    return
                }
                
                if let unwrappedData = data {
                    let image = UIImage(data: unwrappedData)
                    DispatchQueue.main.async(execute: {
                        self.image = image
                    })
                }
                
            }
            dataTask.resume()
        }
    }
}

Using extension

Now we can use our extension for downloading images with any instance of UIImageView
import UIKit

class SingleImageViewController: UIViewController {
    
    @IBOutlet var imageView: UIImageView!
    
    let urlStr = "https://cdn.pixabay.com/photo/2017/05/07/08/56/pancakes-2291908_960_720.jpg"

    override func viewDidLoad() {
        super.viewDidLoad()
        
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        imageView.loadImageWithUrl(urlString: urlStr)
        
    }

}

Caching Images


The problem is that everytime that image is displayed it is loaded from Internet. For avoiding excessing network requests we can use special mechanism called Caching. Caching allows us to store previously downloaded resource(in out case this resource is an image). This resource will be saved locally and when the same URL request will be invoked next time than saved resource will be provided by the app. It will reduce network requests and make app more fast.
Ways to caching images are following:


  1. Cache images using NSCache
  2. Cache images  using URLCache
  3. Cache images  using Frameworks
Let's explore every of them.

Loading and Caching Image using NSCache


Let's modify our extension method for downloading images. We will use special class NSCache for providing caching mechanism. Before loading image from Internet we check its presence in cache and if we download image from Internet we store it to cache for avoiding loading in future.
import UIKit

let imageCache = NSCache<NSString, UIImage>()

extension UIImageView {
    
    func loadImageWithUrl(urlString: String) {
        
        image = nil
        
        if let cachedImage = imageCache.object(forKey: urlString as NSString) {
            image = cachedImage
            return
        }
        
        if let url = URL(string: urlString) {
            let session = URLSession.shared
            let dataTask = session.dataTask(with: url) { (data, response, error) in
                if let unwrappedError = error {
                    print(unwrappedError)
                    return
                }
                
                if let unwrappedData = data, let downloadedImage = UIImage(data: unwrappedData) {
                    DispatchQueue.main.async(execute: {
                        imageCache.setObject(downloadedImage, forKey: urlString as NSString)
                        self.image = downloadedImage
                    })
                }
                
            }
            dataTask.resume()
        }
    }
}

Create cache object.
let imageCache = NSCache<NSString, UIImage>()

Check cache for previously loaded images.
if let cachedImage = imageCache.object(forKey: urlString as NSString) {
    image = cachedImage
    return
}

Save image to cache.
if let unwrappedData = data, let downloadedImage = UIImage(data: unwrappedData) {
    DispatchQueue.main.async(execute: {
        imageCache.setObject(downloadedImage, forKey: urlString as NSString)
        self.image = downloadedImage
    })
}

Clear image view before set new image.
image = nil

Difference Between Dictionary and NSCache


Why should we use NSCache class instead of using simple dictionary for caching images?
var imageCache: [String: UIImage]

Let's see Apple Docs about NSCache class
A mutable collection you use to temporarily store transient key-value pairs that are subject to eviction when resources are low.
The NSCache class incorporates various auto-eviction policies, which ensure that a cache doesn’t use too much of the system’s memory. If memory is needed by other applications, these policies remove some items from the cache, minimizing its memory footprint.
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.

What is the main thing in this? 
The difference is that NSCache detects excessive memory usage and remove some values in order to make room.

So, when to use NSCache? 
If data can be recreated (loading from Internet or calculated again) then you can store values in NSCache. If data cannot be recreated (for example, it is user input) you should not store values in NSCache because it can be removed.

There is our case - we loading images from Internet and if needed we can load it again.

Using Extension in a Table Cell


But what if we use image in every cell of UITableView. Now we can use our extension for that purpose. There are couple of problems. How to resolve them I describe below.

Table View with many images

Our custom table cell will have property imageUrl and property observer for it. When imageUrl was  set than image from that url is loaded.
import UIKit

class CustomCell: UITableViewCell {
    
    private let customImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.contentMode = .scaleAspectFill
        imageView.layer.masksToBounds = true
        return imageView
    }()
    
    var imageUrl: String? {
        didSet {
            if let imageUrl = imageUrl {
                customImageView.loadImageWithUrl(urlString: imageUrl)
            }
        }
    }
    
}

Set imageUrl property of table view cell.
import UIKit

class ImageListViewController: UIViewController {
    
    @IBOutlet var tableView: UITableView!
    
    let cellId = "CellId"
    
    var imageUrls = [String]()

    
}

extension ImageListViewController: UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return imageUrls.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! CustomCell
        let imageUrl = imageUrls[indexPath.row]
        cell.imageUrl = imageUrl
        return cell
    }
}

Make Custom UIImageView (subclass of UIImageView)


Wrong image can be set to UIImageView when we use UIImageView in table view cell. Because of reusing UIImageView for downloading different images. For preventing such behaviour we need to store current image url. But extensions cannot have stored properties, so we have to create custom class that will extend UIImageView.
import UIKit

let imageCache = NSCache<NSString, UIImage>()

class CustomImageView: UIImageView {
    
    var imageUrlString: String?
    
    func loadImageWithUrl(urlString: String) {
        
        imageUrlString = urlString
        
        image = nil
        
        if let cachedImage = imageCache.object(forKey: urlString as NSString) {
            image = cachedImage
            return
        }
        
        if let url = URL(string: urlString) {
            let session = URLSession.shared
            let dataTask = session.dataTask(with: url) { (data, response, error) in
                if let unwrappedError = error {
                    print(unwrappedError)
                    return
                }
                
                if let unwrappedData = data, let downloadedImage = UIImage(data: unwrappedData) {
                    DispatchQueue.main.async(execute: {
                        imageCache.setObject(downloadedImage, forKey: urlString as NSString)
                        if self.imageUrlString == urlString {
                            self.image = downloadedImage
                        }
                    })
                }
                
            }
            dataTask.resume()
        }
    }
}

Store current image url.
 imageUrlString = urlString

Set downloaded image as image of UIImageView only if url is the same.
if self.imageUrlString == urlString {
    self.image = downloadedImage
}

Moving Cache to Global Signleton Class


For making ImageCache available from any place in the app let's create special singleton class for that. When we use singleton class we have only one instance of object in the app.
import UIKit

class ImageCache {
    private let cache = NSCache<NSString, UIImage>()
    private var observer: NSObjectProtocol!
    
    static let shared = ImageCache()
    
    private init() {
        observer = NotificationCenter.default.addObserver(
            forName: .UIApplicationDidReceiveMemoryWarning,
            object: nil, queue: nil) { [weak self] notification in
                self?.cache.removeAllObjects()
        }
    }
    
    deinit {
        NotificationCenter.default.removeObserver(observer)
    }
    
    func getImage(forKey key: String) -> UIImage? {
        return cache.object(forKey: key as NSString)
    }
    
    func save(image: UIImage, forKey key: String) {
        cache.setObject(image, forKey: key as NSString)
    }
}

Using new ImageCache

Getting image from cache.
if let cachedImage = ImageCache.shared.getImage(forKey: urlString) {
    image = cachedImage
    return
}

Saving image to cache.
ImageCache.shared.save(image: downloadedImage, forKey: urlString)

Cancel Previous Download Task if Need

The could be a problem with reusing UIImageView. If one instance UIImageView will be used for downloading different images in a very short time interval (for example in table view cell) than more than one downloading task will be executed. For prevent this behavior we can cancel current download task before begin new.
import UIKit

class CustomImageView: UIImageView {
    
    private var currentTask: URLSessionTask?
    
    var imageUrlString: String?
    
    func loadImageWithUrl(urlString: String) {
        
        weak var oldTask = currentTask
        currentTask = nil
        oldTask?.cancel()
        
        imageUrlString = urlString
        
        image = nil
        
        if let cachedImage = ImageCache.shared.getImage(forKey: urlString) {
            image = cachedImage
            return
        }
        
        if let url = URL(string: urlString) {
            let session = URLSession.shared
            let dataTask = session.dataTask(with: url) { (data, response, error) in
                if let unwrappedError = error {
                    print(unwrappedError)
                    return
                }
                
                if let unwrappedData = data, let downloadedImage = UIImage(data: unwrappedData) {
                    DispatchQueue.main.async(execute: {
                        ImageCache.shared.save(image: downloadedImage, forKey: urlString)
                        if self.imageUrlString == urlString {
                            self.image = downloadedImage
                        }
                    })
                }
                
            }
            currentTask = dataTask
            dataTask.resume()
        }
    }
}

Variable for storing current download task.
private var currentTask: URLSessionTask?

Before starting new download task we need to cancel previous.
weak var oldTask = currentTask
currentTask = nil
oldTask?.cancel()

Saving current download task.
currentTask = dataTask
dataTask.resume()

Caching Images with NSURLCache


There is a composite in-memory and on-disk caching mechanism for URL requests provided by Apple. It is called URLCache. Any URL request will be handled by URLCache. Let's see Apple Docs for URLCache Class:

An object that maps URL requests to cached response objects.
The URLCache class implements the caching of responses to URL load requests by mapping NSURLRequest objects to CachedURLResponse objects. It provides a composite in-memory and on-disk cache, and lets you manipulate the sizes of both the in-memory and on-disk portions. You can also control the path where cache data is stored persistently.
In iOS, the on-disk cache may be purged when the system runs low on disk space, but only when your app is not running.
In iOS 8 and later, and macOS 10.10 and later, URLCache is thread safe.

Network caching reduces the number of requests that need to be made to the server, and improve the experience of using an application offline or under slow network conditions. When a request has finished loading its response from the server, a cached response will be saved locally. The next time the same request is made, the locally-cached response will be returned immediately, without connecting to the server. URLCache returns the cached response automatically and transparently.

You can set cache size for your application in your AppDelegate’s didFinishLaunchingWithOptions method:
func application(_ application: UIApplication, didFinishLaunchingWithOptions 
    launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
    let memoryCapacity = 500 * 1024 * 1024
    let diskCapacity = 500 * 1024 * 1024
    let urlCache = URLCache(memoryCapacity: memoryCapacity, 
                            diskCapacity: diskCapacity, diskPath: "myCachePath")
    URLCache.shared = urlCache
        
    return true
}

Source Code on GitHub


There is github link for demo project source code.

Frameworks for Loading and Caching Images


Asynchronous  loading of images and caching them can be complicated thing. So there are 3 most popular frameworks for asynchronous downloading and caching images (all can be installed with CocoaPods). In most cases it is recommended to use this frameworks. Developers of this spent a lot of time making them efficiently, robust and easy to use.
  • SDWebImage - Asynchronous image downloader with cache support as a UIImageView category
  • AlamofireImage - AlamofireImage is an image component library for Alamofire
  • Kingfisher - A lightweight, pure-Swift library for downloading and caching images from the web.

For example, two line of code for loading image, from Kingfisher docs:

The simplest use-case is setting an image to an image view with the UIImageView extension:
let url = URL(string: "url_of_your_image") 
imageView.kf.setImage(with: url) 
Kingfisher will download the image from url, send it to both the memory cache and the disk cache, and display it in imageView. When you use the same code later, the image will be retrieved from cache and shown immediately.

6 comments:

  1. After a long time i found a unique and on purpose information about Custom Designed Websites. Can't wait to have more from you.

    ReplyDelete
  2. Thank you for sharing this important and useful information in this article. Best Custom Websites

    ReplyDelete
  3. Great content. I was looking for this kind of content. It saved my time to search further.
    You must provide such type of content constantly. I appreciate your willingness to provide readers with a good article.
    Custom Design Websites

    ReplyDelete
  4. Your article is unbelievable with precise knowledge. I can see a lot of research behind that and that is beyond my expectations.
    Create Your Own Website \

    ReplyDelete
  5. This is a great article! Thank you very much. I really need these suggestions. An in-depth explanation is of great benefit. Incredibly inspiring to see how you write. Custom Website

    ReplyDelete
  6. This is an excellent article. Your essay is very easy to understand. Thank you for providing us with this useful information.
    SEO Company NYC

    ReplyDelete