UIImageview + afnetworking downloads images and caches the images.
But in certain cases the server images are = 15mb. So i need to compress them based on the some factor and make it to 1mb and then require to cache them.
SDWebImageCache on the other hand make you to define your own cache and store them
Is there any build in mechanism for downloading,editing and then later saving into the cache?
[SDWebImageDownloader.sharedDownloader downloadImageWithURL:imageURL
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize)
{
// progression tracking code
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished)
{
if (image && finished)
{
// do something with image
}
}];
then use
[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey]
Is there any other alternative to doing something like this?
Your scenario with SDWebImage is correct.
For editing purpose you need set delegate to SDWebImageManager object and implement necessary method:
// Set delegate
[SDWebImageManager sharedManager].delegate = self;
// Implement delegate method
- (UIImage *)imageManager:(SDWebImageManager *)imageManager
transformDownloadedImage:(UIImage *)image
withURL:(NSURL *)imageURL {
UIImage scaledImage = ... // Make scale based on 'image' object
return scaledImage;
}
Note that this method called immediately after image was downloaded but before storing it to memory cache and before completion block is called.
Documentation for this method:
Allows to transform the image immediately after it has been downloaded
and just before to cache it on disk and memory. NOTE: This method is
called from a global queue in order to not to block the main thread.
After that you will be able to use SDWebImageDownloader and SDImageCache as in your question:
[SDWebImageDownloader.sharedDownloader downloadImageWithURL:imageURL
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// progression tracking code
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
if (image && finished) {
[[SDImageCache sharedImageCache] storeImage:image forKey:myCacheKey];
}
}];
Then you can manage cache by using methods of SDImageCache class:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
If you need algorithm for image scaling by max data size take a look on this answer.
Related
In my app, I am needing to download about 6,000 images. I realize this is a lot, but it is needed.
Currently I am using doing the following:
NSArray *photos = #[hugeAmountOfPhotoObjects];
for (ZSSPhoto *photo in photos) {
[self downloadImageWithURL:photo.mobileURL progress:^(double progress) {
} completion:^(UIImage *image) {
// Save the image
} failure:^(NSError *error) {
}];
}
...
- (void)downloadImageWithURL:(NSURL *)url progress:(void (^)(double progress))progress completion:(void (^)(UIImage *image))completion failure:(void (^)(NSError *error))failure {
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setTimeoutInterval:600];
self.operationQueue.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount;
AFHTTPRequestOperation *requestOperation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
requestOperation.responseSerializer = [AFImageResponseSerializer serializer];
[requestOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
completion(responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[self processOperation:operation error:error failure:failure];
}];
[requestOperation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
double percentDone = (double)totalBytesRead / (double)totalBytesExpectedToRead;
progress(percentDone);
}];
[self.operationQueue addOperation:requestOperation];
}
The problem here is that it takes forever to download using this method, and some of my users are reporting crashing because of high memory usage.
Is there a better method that I could be using to download such a large number of image files?
You could try this somewhat recursively
NSMutableArray *undownloaded;
- (void) startDownload {
undownloaded = [photos mutableCopy]; //get a list of the undownloaded images
for(int i = 0; i < 3;i++) //download 3 at a time
[self downloadImage];
}
- (void) downloadImage {
if(undownloaded.count > 0){
ZSSPhoto *photo = undownloaded.firstObject;
[undownloaded removeObjectAtIndex:0];
[self downloadImageWithURL:photo.mobileURL progress:^(double progress) {
} completion:^(UIImage *image) {
// Save the image
[self downloadImage];
} failure:^(NSError *error) {
[self downloadImage];
//[undownloaded addObject:photo]; //insert photo back into the array maybe to retry? warning, could cause infinite loop without some extra logic, maybe the object can keep a fail count itself
}];
}
}
warning: untested code, may need some tweaking
The speed problem can be solved (the speed will increase, but it might still be slow) with multithreading, downloading all the images at the same time instead of one per time. However, the memory problem is a bit more complicated.
ARC will release all the images after everything is finished, but right before that you gonna have 6,000 images in the device memory. You could optimize the images, reduce their resolution or download them in steps, like Google Images do (you download the images that will be visible at first, then when the user scrolls down you load the images in the new visible area; downloading the images only when they are needed).
Considering that you are downloading enough images to give a memory problem, you will probably take a lots of space in your user's device if you download all of them, and the 'steps' solution may solve that as well.
Now, let's suppose you must download all of them at the same time and space isn't a problem: I suppose that if you put the downloadImageWithURL:progress: method inside a concurrent queue, the images are gonna be freed from memory just after saving (it's just a supposition). Add this to your code:
dispatch_queue_t defaultPriorityQueueWithName(const char* name)
{
dispatch_queue_t dispatchQueue = dispatch_queue_create(name, DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t priorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_set_target_queue(dispatchQueue, priorityQueue);
return dispatchQueue;
}
And change your code to that:
dispatch_queue_t threadItemLoadImage = defaultPriorityQueueWithName("DownloadingImages");
NSArray *photos = #[hugeAmountOfPhotoObjects];
for (ZSSPhoto *photo in photos)
{
dispatch_async(threadItemLoadImage, ^
{
[self downloadImageWithURL:photo.mobileURL progress:^(double progress) {
} completion:^(UIImage *image) {
// Save the image
} failure:^(NSError *error) {
}];
});
}
You will need to remove setDownloadProgressBlock: in case it updates some view, since they will be downloaded simultaneously. Also, a warning: totalBytesExpectedToRead not always will be correctly retrieved at first, containing 0, which might make your app crash for dividing by zero as well in some rare occasions. In future cases, when you need to use setDownloadProgressBlock:, check totalBytesExpectedToRead value before doing that division.
I have added a table view, and I am display image in the cells. I have also added this code:
So that the cells resize depending on the image.
When I launch my app though, I get this : [![enter image description here][1]][1]
And the images do not load untill I start scrolling...If I scroll down half the page then go back to the top, I get this: Which is correct
[![enter image description here][2]][2]
Any ideas? I have researched on google and tried the odd solution for the older versions of Xcode, But nothing seems to work!
Here is the rest of my code from the TableViewController:
Image isn't loaded correctly in cellForRowAtIndexPath delegate method, you're (probably) downloading the image in the background, so cellForRowAtIndexPath is returned before image is ready.
Downloaded image is probably cached somewhere so next time it's loaded properly.
post.downloadImage() better have a callback closure to be called when image was downloaded, to assign the downloaded image into the proper cell.
Keep in mind that user may scroll this cell out of the screen before image is loaded, so you better use a unique id to abort downloaded image assignment if cell has already changed.
Here's an example for a method that downloads an image in the background, then assigns it to the cell -
+ (void)loadImage:(NSString *)imageUrl onComplete:(void(^)(UIImage *image, BOOL loaded, NSString *callIdentifier))callback callIdentifier:(NSString *)callIdentifier {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul), ^{
[self downloadPicture:url onComplete:^(UIImage *image, BOOL loaded) {
dispatch_sync(dispatch_get_main_queue(), ^{
callback(image, loaded, callIdentifier);
});
}];
});
callback([UIImage imageNamed:#"placeholder"], NO, callIdentifier);
}
+ (void)downloadPicture:(NSString *)url saveTo:(NSString *)filePath onComplete:(void (^)(UIImage *image, BOOL loaded))onComplete {
NSError *error = nil;
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:url] options:NSDataReadingMappedAlways error:&error];
if (!error) {
UIImage *image = [UIImage imageWithData:data scale:GTUserPictureScale];
if (onComplete)
onComplete(image, YES);
} else {
NSLog(#"Error loading user picture: %#", [error description]);
if (onComplete)
onComplete([UIImage imageNamed:#"missing"], NO);
}
}
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// ...
__weak MyClass *wself = self;
self.imageUrl = #"http://...";
[self loadImage:self.imageUrl onComplete:^(UIImage *image, BOOL loaded, NSString *callIdentifier) {
#synchronized(wself) {
if ([callIdentifier isEqualToString:wself.imageUrl]) {
if (loaded) {
wself.image = image;
}
} else
NSLog(#"Expired load image request for id %#", callIdentifier);
}
} callIdentifier:self.imageUrl];
// ...
}
I'm currently using SDWebImage to load pictures for my table cells, using the following code:
[cell.coverImage sd_setImageWithURL:[self.dataInJSONModel.Content[indexPath.row] CoverImage] placeholderImage:[UIImage imageNamed:#"imageplaceholder_general"]];
The problem is when I scroll up and down, the images were inserted into the wrong cells. After reading some post on StackOverflow regarding this issue, I suspect it to be due to that cells are reused when we scroll and hence the asynchonous download of the image may be placed on a cell indexPath that has changed.
Hence I implemented several changes e.g.:
SDWebImageManager *manager = [SDWebImageManager sharedManager];
UIImageView * cellCoverImage = cell.coverImage;
[manager downloadImageWithURL:[self.dataInJSONModel.Content[indexPath.row] CoverImage] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL * oriURL) {
NSArray *visibleIndexPaths = [tableView indexPathsForVisibleRows];
if ([visibleIndexPaths containsObject:indexPath]) {
cellCoverImage.image = image;
}
}];
Or even to compare URLs:
SDWebImageManager *manager = [SDWebImageManager sharedManager];
UIImageView * cellCoverImage = cell.coverImage;
[manager downloadImageWithURL:[self.dataInJSONModel.Content[indexPath.row] CoverImage] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL * oriURL) {
if([oriURL isEqual:[self.dataInJSONModel.Content[indexPath.row] CoverImage]])
{
cell.coverImage.image = image;
}
}];
Still the problem persist. Or I might have wrongly programmed it? Found several suggestions online but no concrete solution yet seen.
Need help!
EDIT
I've already made some changes to it but still doesn't work:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NewsFeedCell * cell = [tableView dequeueReusableCellWithIdentifier:#"NewsFeedCell" forIndexPath:indexPath];
if (self.dataInJSONModel)
{
cell.coverImage.image = nil;
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadImageWithURL:[self.dataInJSONModel.Content[indexPath.row] CoverImage] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL * oriURL) {
if ([cell isEqual:[self.tableView cellForRowAtIndexPath:indexPath]])
{
cell.coverImage.image = image;
}
}];
}
I met the same problem, and tried assign .image = nil, but not work.
Finally, my sloution is to override prepareForReuse in UITableViewCell with cancel operation:
- (void)prepareForReuse
{
[super prepareForReuse];
[_imageView sd_cancelCurrentImageLoad];
}
Posted the question on the SDWebImage Github page and gotten a suggestion from someone who solves my problem! I just override the prepareForReuse method in my cell's implementation file and nullify the image of the affected imageView.
Sample code for future reader:
In my NewsFeedCell.m
- (void) prepareForReuse
{
[super prepareForReuse];
self.coverImage.image = NULL;
}
And this solves the problem! My opened issue at GitHub is https://github.com/rs/SDWebImage/issues/1024, should any of you want to see.
I tried most of these solutions, spent some time fixing this. I got 2 solutions working for me.
When setting image to ImageView in cells stop download.
In cellForRow add this:
cell.imageView.sd_cancelCurrentImageLoad()
and then in cell:
func prepareForReuse() {
imageView.image = UIImage.placeholderImage() // or nill
}
This is not real solutions because your actually stop image download and waste already downloaded data.
Another more elegant solutions is adding extension for UIImageView:
Choose animation which suits you, and try this:
func setImageAnimated(imageUrl:URL, placeholderImage:UIImage) {
self.sd_setImage(with: imageUrl, placeholderImage: placeholderImage , options:SDWebImageOptions.avoidAutoSetImage, completed: { (image, error, cacheType, url) in
if cacheType == SDImageCacheType.none {
UIView.transition(with: self.superview!, duration: 0.2, options: [.transitionCrossDissolve ,.allowUserInteraction, .curveEaseIn], animations: {
self.image = image
}, completion: { (completed) in
})
} else {
self.image = image
}
})
}
You are right in your analysis of the problem, just not executed it quite correctly.
some pseudocode for cellForRowAtIndexPath...
- set the cell.imageView.image to nil to wipe out
previous use of cell image view contents
- get the URL from data source
- initiate asynchronous download with completion ^{
//check the cell is still the correct cell
if ([cell isEqual: [collectionView cellForRowAtIndexPath:indexPath]]) {
cell.imageView.image = image
}
}
A couple of things you are doing wrong
- don't grab a reference to the cell's image view until you know you need it (in the completion block)
- don't check visibleIndexPaths, the indexPath might still be visible but allocated to a different cell (if you have scrolled off then on again for example). The 'cell isEqual' technique I use here should suffice for all cases.
You can also nil out old cell contents for a recycled cell by overriding the cells -prepareForReuse method.
The Obvious error here is that you're not accounting for using Blocks. Essentially, the completion happens on a background thread, and all UI updates must happen on the Main Thread.
The simple solution is;
dispatch_async(dispatch_get_main_queue(), ^{
cell.coverImage.image = image;
});
Further, if you intend to reference the tableView in your completion block, you should use a weak reference to it.
I'm wondering what is the best way to set images in my UITableView from the server. I'm storing images on the amazon s3 and to get images I have implemented logic in the backend that require two statements:
first one I'm using to get url of image and in the second one I just simply display images to the UITableViewCell. For web requests I'm using AFNetworking library and addition to UIImageView (<UIImageView+AFNetworking.h>). I don't have a problem with requests, however I'm not quite sure where should I put my code responsible for getting all urls of the images. It seems to me that cellForARowAtIndexPath method is good only for displaying images from the final url address:
Here is my code.
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"final url here"]];
[cell.imageView setImageWithURLRequest:request placeholderImage:[UIImage imageNamed:#"GeofencingAlert.png"]
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage
*image)
{
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
NSLog(#"Error: %#",error.description);
}];
Should I first download all of the url stored on the amazon s3 and put them into the NSMutuableArray ? If yes what is the best way to execute multiple requests? What if the user add new record to the table ? Do I have to execute multiple requests again to keep my UITableVIewCell images updated? Thanks in advance.
You should use -(void)viewDidLoad method. Something like this
- (void)viewDidLoad
{
urlPaths = nil;
[httpClient loadUrlsWithSuccess:^(NSArray *results){
urlPaths = results; // urlPaths - class member
[tableView reloadData];
}];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath)indexPath
{
//...
if(urlPaths)
{
NSString *urlPath = urlPaths[indexPath.row];
// load async
}
else
// urls are not loaded yet
imageView.image = placeholderImage;
}
I am using SDWebimage-master image downloader
it takes time to appear an image about to 8 sec.
i want to get notification of image at appear time (not .image property change time)
code:
thumbsize:
[imgFull setImageWithURL:thumbURL placeholderImage:nil];
fullsize :
[imgFull setImageWithURL:fullsizeURL placeholderImage:nil];
You can use this method . it will give you progress in progress block and completed block will call while you getting image. but you should check if(finished) because after finish you can get whole image.
[[SDWebImageManager sharedManager]downloadWithURL:[NSURL URLWithString:url]
options:SDWebImageProgressiveDownload
progress:^(NSInteger receivedSize, NSInteger expectedSize
{
//you can show progress here if you want/
//float progress = receivedSize / (float)expectedSize;
//[progressview setProgress:MAX(MIN(1, progress), 0) animated:YES];
}
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished)
{
if (finished)
{
//download Finish.
}
}];
Hope this will help you.