Swift: Download Image from Internet and Cache Them Doesn't Work Properly. Need Suggestions

Swift: download image from Internet and cache them doesn't work properly. need suggestions

Use AlamofireImage

Swift 2.2 or 2.3

Add --> pod 'AlamofireImage', '~> 2.5' in your pod file.

Swift 3.1

Add --> pod 'AlamofireImage', '~> 3.3' in your pod file.

Put the following line in your code which will automatically cache the image and download it as well.

override func viewDidLoad() {
super.viewDidLoad()
//hit your web service here to get all url's
}

func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return arrImageURL.count
}

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
cell.imgViewPhoto.af_setImageWithURL(arrImageURL[indexPath.row], placeholderImage: UIImage())
}

Image downloading and caching issue

I think the problem is in your response handler, you are setting cache for url you are requesting, not for url from response, I modified your code a little bit, try, hope it will help you

func downloadImage(url: URL, imageView: UIImageView, placeholder: UIImage? = nil, row: Int) {
imageView.image = placeholder
imageView.cacheUrl = url.absoluteString + "\(row)"
if let cachedImage = imageCache.object(forKey: url.absoluteString as NSString) {
imageView.image = cachedImage
} else {
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard
let response = response as? HTTPURLResponse,
let imageData = data,
let image = UIImage(data: imageData),
let cacheKey = response.url?.absoluteString,
let index = self.arrURLs.firstIndex(of: cacheKey)
else { return }
DispatchQueue.main.async {
if cacheKey + "\(index)" != imageView.cacheUrl { return }
imageView.image = image
self.imageCache.setObject(image, forKey: cacheKey as NSString)
}
}.resume()
}
}

And

var associateObjectValue: Int = 0
extension UIImageView {

fileprivate var cacheUrl: String? {
get {
return objc_getAssociatedObject(self, &associateObjectValue) as? String
}
set {
return objc_setAssociatedObject(self, &associateObjectValue, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
}

UPDATED:

downloading and caching images from url asynchronously

I know you found your problem and it was unrelated to the above code, yet I still have an observation. Specifically, your asynchronous requests will carry on, even if the cell (and therefore the image view) have been subsequently reused for another index path. This results in two problems:

  1. If you quickly scroll to the 100th row, you are going to have to wait for the images for the first 99 rows to be retrieved before you see the images for the visible cells. This can result in really long delays before images start popping in.

  2. If that cell for the 100th row was reused several times (e.g. for row 0, for row 9, for row 18, etc.), you may see the image appear to flicker from one image to the next until you get to the image retrieval for the 100th row.

Now, you might not immediately notice either of these are problems because they will only manifest themselves when the image retrieval has a hard time keeping up with the user's scrolling (the combination of slow network and fast scrolling). As an aside, you should always test your app using the network link conditioner, which can simulate poor connections, which makes it easier to manifest these bugs.

Anyway, the solution is to keep track of (a) the current URLSessionTask associated with the last request; and (b) the current URL being requested. You can then (a) when starting a new request, make sure to cancel any prior request; and (b) when updating the image view, make sure the URL associated with the image matches what the current URL is.

The trick, though, is when writing an extension, you cannot just add new stored properties. So you have to use the associated object API to associate these two new stored values with the UIImageView object. I personally wrap this associated value API with a computed property, so that the code for retrieving the images does not get too buried with this sort of stuff. Anyway, that yields:

extension UIImageView {
private static var taskKey = 0
private static var urlKey = 0

private var currentTask: URLSessionTask? {
get { objc_getAssociatedObject(self, &UIImageView.taskKey) as? URLSessionTask }
set { objc_setAssociatedObject(self, &UIImageView.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}

private var currentURL: URL? {
get { objc_getAssociatedObject(self, &UIImageView.urlKey) as? URL }
set { objc_setAssociatedObject(self, &UIImageView.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}

func loadImageAsync(with urlString: String?, placeholder: UIImage? = nil) {
// cancel prior task, if any

weak var oldTask = currentTask
currentTask = nil
oldTask?.cancel()

// reset image view’s image

self.image = placeholder

// allow supplying of `nil` to remove old image and then return immediately

guard let urlString = urlString else { return }

// check cache

if let cachedImage = ImageCache.shared.image(forKey: urlString) {
self.image = cachedImage
return
}

// download

let url = URL(string: urlString)!
currentURL = url
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
self?.currentTask = nil

// error handling

if let error = error {
// don't bother reporting cancelation errors

if (error as? URLError)?.code == .cancelled {
return
}

print(error)
return
}

guard let data = data, let downloadedImage = UIImage(data: data) else {
print("unable to extract image")
return
}

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

if url == self?.currentURL {
DispatchQueue.main.async {
self?.image = downloadedImage
}
}
}

// save and start new task

currentTask = task
task.resume()
}
}

Also, note that you were referencing some imageCache variable (a global?). I would suggest an image cache singleton, which, in addition to offering the basic caching mechanism, also observes memory warnings and purges itself in memory pressure situations:

class ImageCache {
private let cache = NSCache<NSString, UIImage>()
private var observer: NSObjectProtocol?

static let shared = ImageCache()

private init() {
// make sure to purge cache on memory pressure

observer = NotificationCenter.default.addObserver(
forName: UIApplication.didReceiveMemoryWarningNotification,
object: nil,
queue: nil
) { [weak self] notification in
self?.cache.removeAllObjects()
}
}

deinit {
NotificationCenter.default.removeObserver(observer!)
}

func image(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)
}
}

A bigger, more architectural, observation: One really should decouple the image retrieval from the image view. Imagine you have a table where you have a dozen cells using the same image. Do you really want to retrieve the same image a dozen times just because the second image view scrolled into view before the first one finished its retrieval? No.

Also, what if you wanted to retrieve the image outside of the context of an image view? Perhaps a button? Or perhaps for some other reason, such as to download images to store in the user’s photos library. There are tons of possible image interactions above and beyond image views.

Bottom line, fetching images is not a method of an image view, but rather a generalized mechanism of which an image view would like to avail itself. An asynchronous image retrieval/caching mechanism should generally be incorporated in a separate “image manager” object. It can then detect redundant requests and be used from contexts other than an image view.


As you can see, the asynchronous retrieval and caching is starting to get a little more complicated, and this is why we generally advise considering established asynchronous image retrieval mechanisms like AlamofireImage or Kingfisher or SDWebImage. These guys have spent a lot of time tackling the above issues, and others, and are reasonably robust. But if you are going to “roll your own,” I would suggest something like the above at a bare minimum.

Swift image cache not working

Ok well sorry if it was a bad question. This is what finally worked:

var imageCache = NSMutableDictionary()

extension UIImageView {

func loadImageUsingCacheWithUrlString(urlString: String) {

self.image = nil

if let img = imageCache.valueForKey(urlString) as? UIImage{
self.image = img
}
else{
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(NSURL(string: urlString)!, completionHandler: { (data, response, error) -> Void in

if(error == nil){

if let img = UIImage(data: data!) {
imageCache.setValue(img, forKey: urlString) // Image saved for cache
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.image = img
})
}

}
})
task.resume()
}
}
}

I guess I had something out of order or something.

Check If Server's Image is Already Updated and Download Again

You can

  1. Set a validity period for the image cache. So image cache is reloaded with the latest data form server every, say, 24 hours or so.

  2. Keep a 'timestamp' on your server whenever user uploads a profile picture and keep a local time stamp for every image URL on device when the image is cached. Check/compare time stamp on every app execution or every time the profile page is opened. When the server timestamp is newer, invalidate the cache and re-download the new image. Make sure local cache timestamp is updated every time the image is cached.

  3. Maintain a file 'hash' string on the server. When you download the image file create a file hash locally and maintain it for every image URL. Compare local value to the server hash on every app execution or everytime the profile page is opened. If they are not the same, invalidate the cache and re-download the new image. Make sure local file hash is updated every time the file is downloaded. However, this will not be possible if your image caching module does not give you direct access to the downloaded physical file.

I assume you have clear idea of an image caching stratagy and hope this answers your question regarding 'how do we know if the picture is already updated and re-download it to the device and replace the cache?'.

If you want to know how to cache in image, you can use UIImage+AFNetworking.

Loading/Downloading image from URL on Swift

Xcode 8 or later • Swift 3 or later

Synchronously:

if let filePath = Bundle.main.path(forResource: "imageName", ofType: "jpg"), let image = UIImage(contentsOfFile: filePath) {
imageView.contentMode = .scaleAspectFit
imageView.image = image
}

Asynchronously:

Create a method with a completion handler to get the image data from your url

func getData(from url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> ()) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
}

Create a method to download the image (start the task)

func downloadImage(from url: URL) {
print("Download Started")
getData(from: url) { data, response, error in
guard let data = data, error == nil else { return }
print(response?.suggestedFilename ?? url.lastPathComponent)
print("Download Finished")
// always update the UI from the main thread
DispatchQueue.main.async() { [weak self] in
self?.imageView.image = UIImage(data: data)
}
}
}

Usage:

override func viewDidLoad() {
super.viewDidLoad()
print("Begin of code")
let url = URL(string: "https://cdn.arstechnica.net/wp-content/uploads/2018/06/macOS-Mojave-Dynamic-Wallpaper-transition.jpg")!
downloadImage(from: url)
print("End of code. The image will continue downloading in the background and it will be loaded when it ends.")
}

Extension:

extension UIImageView {
func downloaded(from url: URL, contentMode mode: ContentMode = .scaleAspectFit) {
contentMode = mode
URLSession.shared.dataTask(with: url) { data, response, error in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let image = UIImage(data: data)
else { return }
DispatchQueue.main.async() { [weak self] in
self?.image = image
}
}.resume()
}
func downloaded(from link: String, contentMode mode: ContentMode = .scaleAspectFit) {
guard let url = URL(string: link) else { return }
downloaded(from: url, contentMode: mode)
}
}

Usage:

imageView.downloaded(from: "https://cdn.arstechnica.net/wp-content/uploads/2018/06/macOS-Mojave-Dynamic-Wallpaper-transition.jpg")

NSCache Doesn't work with all images when loading for the first time

Here the images are downloading and stored in cache just fine. The problem lies in the updation of tableview cells.

When the table view is loading the cells on to the table the images are not downloaded yet. But once the image is downloaded we have to selectively update the cell so that the image is displayed instantly.

Since you are scrolling , the tableview calls 'cellForRowatIndexpath' again which updates the cell showing the downloaded images while scrolling.

If you still wish to use the extension , I suggest you add the tableView and indexpath as the parameters so that we can call reload specific row and have the view updated instantly.

I have updated the table reload code and structure of the function defined in extension. Let me know how it goes.

let imageCache = NSCache<AnyObject, AnyObject>()
var imageURLString : String?

extension UIImageView {

public func imageFromServerURL(urlString: String, tableView : UITableView, indexpath : IndexPath)) {
imageURLString = urlString

if let url = URL(string: urlString) {

image = nil

if let imageFromCache = imageCache.object(forKey: urlString as AnyObject) as? UIImage {

self.image = imageFromCache

return
}

URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in

if error != nil{
print(error as Any)

return
}

DispatchQueue.main.async(execute: {

if let imgaeToCache = UIImage(data: data!){

if imageURLString == urlString {
self.image = imgaeToCache
}

imageCache.setObject(imgaeToCache, forKey: urlString as AnyObject)// calls when scrolling

tableView.reloadRows(at: [indexpath], with: .automatic)

}
})
}) .resume()
}
}

AlamofireImage Collection cell puts image in AutoPurgingImageCache after download

I found that I only needed to make the size of the cache larger. The images I was downloading were bigger than I predicted, and the cache was filling up far too quickly, then purging.

// AlamofireImage cache with 60 MB cache
let imageCache = AutoPurgingImageCache(
memoryCapacity: 60 * 1024 * 1024,
preferredMemoryUsageAfterPurge: 20 * 1024 * 1024
)

You can see your cache filling up if you periodically use imageCache.memoryUsage.

Print('Image cache size = \(imageCache.memoryUsage)')


Related Topics



Leave a reply



Submit