Downloading and Caching Images from Url Asynchronously

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.

How do I asynchronously download and cache videos for use in my app?

So I was able to solve the problem with the following:

Swift 4:

import Foundation

public enum Result<T> {
case success(T)
case failure(NSError)
}

class CacheManager {

static let shared = CacheManager()
private let fileManager = FileManager.default
private lazy var mainDirectoryUrl: URL = {

let documentsUrl = self.fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
return documentsUrl
}()

func getFileWith(stringUrl: String, completionHandler: @escaping (Result<URL>) -> Void ) {

let file = directoryFor(stringUrl: stringUrl)

//return file path if already exists in cache directory
guard !fileManager.fileExists(atPath: file.path) else {
completionHandler(Result.success(file))
return
}

DispatchQueue.global().async {

if let videoData = NSData(contentsOf: URL(string: stringUrl)!) {
videoData.write(to: file, atomically: true)
DispatchQueue.main.async {
completionHandler(Result.success(file))
}
} else {
DispatchQueue.main.async {
let error = NSError(domain: "SomeErrorDomain", code: -2001 /* some error code */, userInfo: ["description": "Can't download video"])

completionHandler(Result.failure(error))
}
}
}
}

private func directoryFor(stringUrl: String) -> URL {

let fileURL = URL(string: stringUrl)!.lastPathComponent
let file = self.mainDirectoryUrl.appendingPathComponent(fileURL)
return file
}
}

Usage:

      CacheManager.shared.getFileWith(stringUrl: videoURL) { result in

switch result {
case .success(let url):
// do some magic with path to saved video

break;
case .failure(let error):
// handle errror

print(error, " failure in the Cache of video")
break;
}
}

How to load image asynchronously with Swift using UIImageViewExtension and preventing duplicate images or wrong Images loaded to cells

You are asynchronously updating your image view, regardless of whether the image view has been re-used for another cell.

When you start a new request for an image view, assuming you didn’t find an image in the cache immediately, before starting network request, you should (a) remove any prior image (like Brandon suggested); (b) possibly load a placeholder image or UIActivityIndicatorView; and (c) cancel any prior image request for that image view. Only then should you start a new request.

In terms of how you save a reference to the prior request in an extension, you can’t add stored properties, but you can use objc_setAssociatedObject to save the session task when you start the session, set it to nil when the request finishes, and objc_getAssociatedObject when retrieving the session object to see if you need to cancel the prior one.

(Incidentally, Kingfisher wraps this associated object logic in their computed property for the task identifier. This is a fine way to save and retrieve this task identifier.


In terms of failed requests, the fact that you are performing unbridled image requests could cause that problem. Scroll around a bit and your requests will get backlogged and timeout. Doing the cancelation (see above) will diminish that problem, but it might still eventually happen. If you continue to have requests fail after fixing the above, then you might need to constrain the number of concurrent requests. A common solution is to wrap requests in asynchronous Operation subclass and add them to OperationQueue with a maxConcurrentOperationCount. Or if you’re looking for a cheap and cheerful solution, you could try bumping up the timeout threshold in your requests.

iOS - Caching and loading images asynchronously

I know that this thread has been answered, but I have tried a library that has worked great. I was using ASIHttpRequest before and the difference is big.

https://github.com/rs/SDWebImage

Also, if someone needs to Resize or Crop the remote images, and have the same features that SDWebImage provide, I have integrated SDWebImage library with UIImage+Resize library (by Trevor Harmon), and created an example project. I modified the code of SDWebImage to deal with transformations (crop, resize).

The project is public on https://github.com/toptierlabs/ImageCacheResize. Feel free to use it!

IOS How do I asynchronously download and cache images and videos for use in my app

It is very simple to download and cache. The following code will asynchronously download and cache.

NSCache *memoryCache; //assume there is a memoryCache for images or videos

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{

NSString *urlString = @"http://URL";

NSData *downloadedData = [NSData dataWithContentsOfURL:[NSURL URLWithString:urlString]];

if (downloadedData) {

// STORE IN FILESYSTEM
NSString* cachesDirectory = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *file = [cachesDirectory stringByAppendingPathComponent:urlString];
[downloadedData writeToFile:file atomically:YES];

// STORE IN MEMORY
[memoryCache setObject:downloadedData forKey:urlString];
}

// NOW YOU CAN CREATE AN AVASSET OR UIIMAGE FROM THE FILE OR DATA
});

Now there is something peculiar with UIImages that makes a library like SDWebImage so valuable , even though the asynchronously downloading images is so easy. When you display images, iOS uses a lazy image decompression scheme so there is a delay. This becomes jaggy scrolling if you put these images into tableView cells. The correct solution is to image decompress (or decode) in the background, then display the decompressed image in the main thread.

To read more about lazy image decompression, see this: http://www.cocoanetics.com/2011/10/avoiding-image-decompression-sickness/

My advice is to use SDWebImage for your images, and the code above for your videos.

Loading image from remote URL asynchronously in SwiftUI Image using combine's Publisher

You have to use an ObservableObject for subscribing to the publisher provided by ImageLoader.

class ImageProvider: ObservableObject {
@Published var image = UIImage(named: "icHamburger")!
private var cancellable: AnyCancellable?
private let imageLoader = ImageLoader()

func loadImage(url: URL) {
self.cancellable = imageLoader.publisher(for: url)
.sink(receiveCompletion: { failure in
print(failure)
}, receiveValue: { image in
self.image = image
})
}
}

struct MyImageView: View {
var url: URL
@StateObject var viewModel = ImageProvider()
var body: some View {
Image(uiImage: viewModel.image)
.onAppear {
viewModel.loadImage(url: url)
}
}
}


Related Topics



Leave a reply



Submit