iOS How to Asynchronously Download and Cache Images and Videos for Use in My App

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.

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 store a set of images permanently in iOS app - Swift

If you already have a backend with this data then I think setting up an api that your app hits to receive download links via JSON makes the most sense. From there you can either download the image and save to the app's sandbox or save the image directly to the user's camera roll.

Also you might want to check out a great pod for this kind of stuff called SDWebImage. It will download images asynchronously and cache them locally.

Some other good tutorials and links on image downloading:

http://natashatherobot.com/ios-how-to-download-images-asynchronously-make-uitableview-scroll-fast

How to save image to app sandbox

More info on caching images

How to cache images properly in ios

The Code below in your heightForRowAtIndexPath is causing issue:

NSString *filePath = [NSString stringWithFormat:@"%@app/media/access/pictures?p=%@",baseurl,data.picPath];
self.img = [UIImage imageWithData: [NSData dataWithContentsOfURL:[NSURL URLWithString:filePath]]];

Code will executed every time you scroll tableView and it will cause freezing problem since it is not asynchronous call.

Edit:
To avoid that you can do something like this:

when finished downloading you can use,

[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:indexPath, nil] 
withRowAnimation:UITableViewRowAnimationAutomatic];

This will call again

    -(CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath

Like,

[cell.image sd_setImageWithURL:[NSURL URLWithString:[NSMutableString stringWithFormat:@"%@app/media/access/pictures?p=%@",baseurl,data.picPath]]

completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {

if (error) {

NSLog(@"error: %@",error.description);

}
else{

//get image height and return this height from HeightForRowAtIndexPath
Height=image.size.height;

//reloading row will call heightForRowAtIndexPath,where now you can return height got above
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:indexPath, nil] withRowAnimation:UITableViewRowAnimationAutomatic];

NSLog(@"Not an error in image download");
}

}];

Edit1:

to check image already cached, you can use below method of SDWebImage library:

- (BOOL)diskImageExistsWithKey:(NSString *)key;

The cache key is an application unique identifier for the image to cache. It is generally the absolute URL of the image.

Now if image exist in cache,then get image path using below methods of SDWebImage Library:

- (NSString *)defaultCachePathForKey:(NSString *)key;
- (NSString *)cachedFileNameForKey:(NSString *)key;

You can get the path to your image like this:

NSString *ImageKey;//key for the image you want the path for
[[SDImageCache sharedImageCache] defaultCachePathForKey:ImageKey];

in case you do not use the default location, you can use:

- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path;

For more detail check here: SDWebImage

Swift: asynchronously loading and displaying photos

What you need is to add a fetchResult property to your collection view controller and fetch your image Assets inside viewDidLoad method.

var fetchResult: PHFetchResult<PHAsset> = PHFetchResult()

func fetchAssets() {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
}

override func viewDidLoad() {
super.viewDidLoad()
fetchAssets()
}

Next step is extend UIImageView to request the image data asynchronously setting its image on completion.

extension UIImageView {    
func fetchImage(asset: PHAsset, contentMode: PHImageContentMode, targetSize: CGSize, version: PHImageRequestOptionsVersion = .current, deliveryMode: PHImageRequestOptionsDeliveryMode = .opportunistic) {
let options = PHImageRequestOptions()
options.version = version
options.deliveryMode = deliveryMode
PHImageManager.default().requestImage(for: asset, targetSize: targetSize, contentMode: contentMode, options: options) { image, _ in
guard let image = image else { return }
switch contentMode {
case .aspectFill: self.contentMode = .scaleAspectFill
case .aspectFit: self.contentMode = .scaleAspectFit
@unknown default: fatalError()
}
self.image = image
}
}
}

Now you can fetch the images one at a time inside collection view cellForItemAt method:

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! CollectionViewCell
let asset = fetchResult.object(at: indexPath.row)
cell.imageView.fetchImage(asset: asset, contentMode: .aspectFill, targetSize: cell.imageView.frame.size * UIScreen.main.scale )
return cell
}

Don't forget to return the fetchResult count for numberOfItemsInSection method.

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


class CollectionViewCell: UICollectionViewCell {
@IBOutlet weak var imageView: UIImageView!
}


extension CGSize {
static func *(lhs: CGSize, rhs: CGFloat) -> CGSize {
.init(width: lhs.width * rhs, height: lhs.height * rhs)
}
}

Is it possible to cache Videos? IOS - Swift

Using Haneke, I wasn't able to retrieve file path for cached video. I handled it by saving video in cached directory.

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 {
completionHandler(Result.failure(NSError.errorWith(text: "Can't download video")))
}
}
}
}

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

let fileURL = URL(string: stringUrl)!.lastPathComponent

let file = self.mainDirectoryUrl.appendingPathComponent(fileURL)

return file
}
}

Sample usage of this class looks like this:

CacheManager.shared.getFileWith(stringUrl: "http://techslides.com/demos/sample-videos/small.mp4") { result in

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


Related Topics



Leave a reply



Submit