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
MySQL and Swift - Upload Image and File || Would It Be Better to Use Alamofire
How to Identify Ekevent Uniquely with Sync Across the Devices
How to Round the Corners of a Button
Swift: Declare an Empty Dictionary
Toplayoutguide in Child View Controller
iOS 8.1 Simulator Localization Broken (Nslocalizedstring)
Obscure a Uitextfield Password
How to Make Sure API Requests Come from Our Mobile (Ios/Android) App
How to Update Uibutton Title/Text Programmatically
Swift 3.0, Alamofire 4.0 Extra Argument 'Method' in Call
How to Change Height of Uitableviewcell Real-Time
Differences Between Websockets and Long Polling for Turn Based Game Server
Scrollview and Keyboard in Swift
How to Change "Initwithnibname" in Storyboard
Assertion Failure When Using Uisearchdisplaycontroller in Uitableviewcontroller
How to Add Headerview in Uicollectionview Like Uitableview's Tableheaderview