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:
- Cache images using NSCache
- Cache images using URLCache
- 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.
After a long time i found a unique and on purpose information about Custom Designed Websites. Can't wait to have more from you.
ReplyDeleteThank you for sharing this important and useful information in this article. Best Custom Websites
ReplyDeleteGreat content. I was looking for this kind of content. It saved my time to search further.
ReplyDeleteYou must provide such type of content constantly. I appreciate your willingness to provide readers with a good article.
Custom Design Websites
Your article is unbelievable with precise knowledge. I can see a lot of research behind that and that is beyond my expectations.
ReplyDeleteCreate Your Own Website \
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
ReplyDeleteThis is an excellent article. Your essay is very easy to understand. Thank you for providing us with this useful information.
ReplyDeleteSEO Company NYC