SDWebImage NSURLRequests failing intermittently - ios

I'm loading images from a remote server using SDWebImage into a UICollectionView using the following code:
[myCell.imageView setImageWithURL:imgURL placeholderImage:nil options:SDWebImageRetryFailed success:^(UIImage *image)
{
[_imageCache storeImage:image forKey:[imgURL absoluteString] toDisk:YES];
} failure:^(NSError *error){
NSLog(#"ERROR: %#", error);
}];
For most cells, this code works fine - it loads the images and saves them to my local disk. However, after several (it seems random?) images, they stop loading. I then get the following error:
ERROR: Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo=0x1d33fdc0 {NSErrorFailingURLStringKey=http://path/to/image.jpg, NSErrorFailingURLKey=http://path/to/image.jpg, NSLocalizedDescription=The request timed out., NSUnderlyingError=0x1d34c0f0 "The request timed out."}
When this happens, my app seems to stop sending NSURLRequests altogether. After a period of time, probably about 20-30 seconds, I can refresh the table and the failed images will load in correctly and the app will resume responding to all NSURLRequests perfectly fine.
I find that this tends to happen more often if I scroll down my collection view fast. Could it be trying to download too many at once? Is there a way to limit the number of concurrent downloads? This method appears to be deprecated in the latest SDWebImage code.

Figured it out. I was using MWPhotoBrowser in another part of my app. MWPhotoBrowser comes with an older/modified version of SDWebImage. I downloaded the latest version of SDWebImage from Github, renamed/refactored all of the files, and imported my newly updated and modified SDWebImage alongside the one MWPhotoBrowser relies on.
The new version of SDWebImage has solved my problem completely!

It's better if use asynchronous download by share manager and display when finish download with SDWebImage. Now, you can scroll fast to test.
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
MyCell *myCell = (MyCell *)[cv dequeueReusableCellWithReuseIdentifier:#"MyCell" forIndexPath:indexPath];
//Set placeholder
myCell.imageView.image = [UIImage imageNamed:#"placeholder.png"];
NSURL *cellImageURL = [NSURL URLWithString:[NSString stringWithFormat:#"%#",[self.collectionData objectAtIndex:indexPath.row]]];
[myCell.cellIndicator startAnimating];
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadWithURL:cellImageURL
delegate:self
options:0
success:^(UIImage *image, BOOL cached)
{
//Display when finish download
myCell.imageView.image = image;
[myCell.cellIndicator stopAnimating];
} failure:nil];
return cell;
}
*EDIT:
If problem still not solve on device: try to separate download and display
#interface ViewController ()
#property (retain , nonatomic) NSMutableArray *linksArray;
#property (retain , nonatomic) NSMutableArray *imagesArray;
#end
- (void)viewDidLoad
{
[super viewDidLoad];
[self initLinksArray];
//Add placehoder.png to your imagesArray
[self initImagesArray];
//Download to NSMultableArray
[self performSelectorInBackground:#selector(downloadImages) withObject:nil];
}
- (void)initImagesArray{
self.imagesArray = [[[NSMutableArray alloc] init] autorelease];
for (NSInteger i = 0; i < self.linksArray.count; i++) {
[self.imagesArray addObject:[UIImage imageNamed:#"placeholder.png"]];
}
}
- (void)downloadImages{
for (NSInteger i = 0; i < self.linksArray.count; i++) {
NSURL *cellImageURL = [NSURL URLWithString:[self.linksArray objectAtIndex:i]];
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadWithURL:cellImageURL
delegate:self
options:0
success:^(UIImage *image, BOOL cached)
{
[self.imagesArray replaceObjectAtIndex:i withObject:image];
} failure:nil];
}
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [self.imagesArray count];;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
SDCell *cell = (SDCell *)[cv dequeueReusableCellWithReuseIdentifier:#"SDCell" forIndexPath:indexPath];
cell.cellImageView.image = [self.imagesArray objectAtIndex:indexPath.row];
return cell;
}

Related

iOS - AFNetworking crashing on line 577 of AFURLResponseSerialization.m

I'm implementing a lazy loading strategy in my app where I use AFNetworking to asynchronously load news article images in a UITableView. This is the following code that does that in my cellForRowAtIndexPath method:
#implementation PocketTableViewController
- (AFHTTPRequestOperationManager *)operationManager
{
if (!_operationManager)
{
_operationManager = [[AFHTTPRequestOperationManager alloc] init];
_operationManager.responseSerializer = [AFImageResponseSerializer serializer];
};
return _operationManager;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
...
if (articleImageURL != nil)
{
//set article image
cell.ThumbImage.image = [UIImage imageNamed:#"greyImage.png"];
[cell.ThumbImage.associatedObject cancel];
cell.ThumbImage.associatedObject =
[self.operationManager GET:[articleImageURL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]
parameters:nil
success:^(AFHTTPRequestOperation *operation, id responseObject) {
cell.ThumbImage.image = responseObject;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Failed with error %#.", error);
}];
}
It works for most images when I scroll through the table view, but then it crashes here:
Any thoughts on why it's crashing?
UPDATE:
I found that it's crashing on this image url for some reason:
http://www.profitconfidential.com/wp-content/uploads/2015/12/Bill-Gates-Bought-Stock-in-Third-Quarter.jpg
Use UIImage+AFNetworking.
It lazy loading and supported image caching.
In your case, you can use this:
UIImage *image = [UIImage imageWithData:responseObject];
But the best way is use UIImage+AFNetworking.
Apparently I fixed the problem. There was nothing wrong with any of the images. I just had to disable all breakpoints in Xcode and it worked. Weird, but it fixed the problem.

How to update UIImages after NSURLConnection delegate completes

So I'm pulling down about 50 images from my API using NSURLConnection, its working great, except its locking up the UI when it runs. I'm assuming that is because I'm updating the UI in real time form the NSURLConnection self delegate. So I'm thinking what I need to do is put placeholder loading images in the UIImage, then update them somehow once the delegate has acquired all the data, but how do I do that, can someone give me some coding examples?
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// The request is complete and data has been received
// You can parse the stuff in your instance variable now
NSData *imageData = _dataDictionary[ [connection description] ];
if(imageData!=nil)
{
NSLog(#"%#%#",[connection description],imageData);
UIImageView *imageView = [[UIImageView alloc] initWithFrame: CGRectMake(self.x, 0, self.screenWidth, self.screenHight)];
// Process thi image
// resize the resulting image for this device
UIImage *resizedImage = [self imageScaleCropToSize:[UIImage imageWithData: imageData ]];
self.x = (self.x + imageView.frame.size.width);
if(self.x > self.view.frame.size.width) {
self.scrollView.contentSize = CGSizeMake(self.x, self.scrollView.frame.size.height);
}
[imageView setImage:resizedImage];
// add the image
[self.scrollView addSubview: imageView];
}
}
You can use SDWebImage library to achieve this.
Suppose imageArray have all the image url path.
You can use SDWebImageManager to download all the images and show them in ImageView. Also you can show downloading progress using this block.
- (void)showImages:(NSArray *)imageArray
{
SDWebImageManager *manager = [SDWebImageManager sharedManager];
for (NSString *imagePath in imageArray)
{
[manager downloadImageWithURL:[NSURL URLWithString:imagePath]
options:SDWebImageLowPriority
progress:^(NSInteger receivedSize, NSInteger expectedSize){}
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL)
{
if(!error)
self.imgView_Image.image = image;
else
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"please check your Connection and try again" message:#"No Internet Connection" delegate:nil cancelButtonTitle:#"Cancel" otherButtonTitles: nil];
[alert show];
}
}];
}
}
First create protocol in that class .h, where you call NSURLConnection request for download image (Where you implement this method connectionDidFinishLoading).
#protocol YourClassNameDelegate <NSObject>
- (void)didFinishLoadingImage:(UIImage *)downloadImage;
#end
and create property for that protocol in same class,
#interface YourViewController : UIViewController
#property (nonatomic, retain) id<YourClassNameDelegate>delegate;
#end
then synthesise it in .m, #synthesize delegate;
After that call didFinishLoadingImage: in connectionDidFinishLoading,
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// The request is complete and data has been received
// You can parse the stuff in your instance variable now
NSData *imageData = _dataDictionary[ [connection description] ];
if(imageData!=nil)
{
NSLog(#"%#%#",[connection description],imageData);
UIImageView *imageView = [[UIImageView alloc] initWithFrame: CGRectMake(self.x, 0, self.screenWidth, self.screenHight)];
// Process thi image
// resize the resulting image for this device
UIImage *resizedImage = [self imageScaleCropToSize:[UIImage imageWithData: imageData ]];
self.x = (self.x + imageView.frame.size.width);
if(self.x > self.view.frame.size.width) {
self.scrollView.contentSize = CGSizeMake(self.x, self.scrollView.frame.size.height);
}
[self.delegate didFinishLoadingImage:resizedImage];
[imageView setImage:resizedImage];
// add the image
[self.scrollView addSubview: imageView];
}
}
and finally from where you push to YourViewController set delegate to self, like :
YourViewController *controller = [[YourViewController alloc] init];
controller.delegate = self;
//.....
in YourViewController.m, where you want to set downloaded image, in that class implement this method.
#pragma mark - YourClassName delegate method
- (void)didFinishLoadingImage:(UIImage *)downloadImage
{
//yourImageView.image = downloadImage;
}

Asynchrone image download with return value required

I'm trying to setup an asynchrone download of images on my application.
I'm using SDWebImage as suggested in this issue.
I put breakpoints on the progress and completed method and everything is normal. It's working perfectly but I have another problem coming from my logic directly. I don't know how to set my image asynchronously on my UIImageView.
Everything is dynamic and each image is called independently
Here is a part of my code:
[myMenu->_userAvatar setImage:[[CacheCache sharedInstance] getUIImageFromPath:currentUser.avatarPhoto.avatarURL]];
Note that CacheCache is my own cache method.
NSURL* myURL=[NSURL URLWithString:path];
//NSData* myData=[NSData dataWithContentsOfURL:myURL];
NSData* myData;
[SDWebImageDownloader.sharedDownloader downloadImageWithURL:myURL
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize)
{
DDLogInfo(#"Downloading...");
// progression tracking code
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished)
{
if (image && finished)
{
DDLogInfo(#"Downloaded !");
image = [[UIImage alloc] initWithData:myData];
}
}];
...
return image;
Thank you for your help.
Rather than going for that complex solution, you can try this one. Create NSObject file with name 'DownloadImagesAsynchronously' and replace .h file with following.
#import <Foundation/Foundation.h>
#protocol NotifyParentProtocol <NSObject>
-(void)ImageDownloaded:(BOOL)_isDownloaded;
#end
#interface DownloadImagesAsynchronously : NSObject{
NSMutableData *receivedData;
UIImageView *cCellImageView;
NSURLConnection *imgDownloadConnection;
id<NotifyParentProtocol> __weak delegate;
}
-(void) downloadImageAsynchornously:(NSString *)_imageURL andCellImage:(UIImageView *)_cellImgV;
#property(weak) id<NotifyParentProtocol> delegate;
#end
and replace .m with following code
#import "DownloadImagesAsynchronously.h"
#implementation DownloadImagesAsynchronously
#synthesize delegate;
- (void)loadWithURL:(NSURL *)url{
NSURLConnection *conect = [NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:url]delegate:self];
[conect start];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
[receivedData setLength:0];
}
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
[receivedData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
cCellImageView.image = [UIImage imageWithData:receivedData];
[cCellImageView.layer setCornerRadius:14.0f];
[delegate ImageDownloaded:YES];
}
-(void) downloadImageAsynchornously:(NSString *)_imageURL andCellImage:(UIImageView *)_cellImage{
cCellImageView = _cellImage;
receivedData = [[NSMutableData alloc] init];
NSString *baseURL = #"http://example.com/abc/Gallary";
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"%#/%#",baseURL,_imageURL]];
[self loadWithURL:url];
}
#end
Now cal on your UIImageView like this, if using TableView then it will lazily download image to each tableview cell.
DownloadImagesAsynchronously *downloadAsynchImageObj = [[DownloadImagesAsynchronously alloc] init];
downloadAsynchImageObj.delegate = self;
[downloadAsynchImageObj downloadImageAsynchornously:model.ImageName1 andCellImage:self.mainImageView];
and implement delegate method. It will notify you when ever image is being downloaded. You can perform your desired action.
- (void)ImageDownloaded:(BOOL)_isDownloaded{
// Image Downloaded.
}
Hope this will work for you, if you have any question related this. Please let me know. Thanks
Thank you guys.
I mixed both of your answers. I simply passed my UIImageView to my method with asynchrone block.
Here is my code:
//view is an UIImageView coming directly from one of the parameters
if (view == nil) {
myData=[NSData dataWithContentsOfURL:myURL];
image = [[UIImage alloc] initWithData:myData];
}
else{
[SDWebImageDownloader.sharedDownloader downloadImageWithURL:myURL
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize)
{
DDLogInfo(#"Downloading...");
// progression tracking code
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished)
{
if (image && finished)
{
DDLogInfo(#"Downloaded !");
view.image = image;
}
}];
}
Now I only have a problem about the resize of my picture and its scale but my original issue is fixed.
I hope this will help someone else.

Can I use NSUserDefaults to save the app’s table cell values?

I have a table which is populated by a title and image entry. This is done by the following methods:
tablePhotoViewController.m
- (IBAction)takePicture:(UIBarButtonItem *)sender {
// check #1 - make sure our source type is supported
if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
NSArray *mediaTypes = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera];
// check #2 see if media type includes images
if ([mediaTypes containsObject:(NSString *)kUTTypeImage]) {
// create our image picker
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.delegate = self;
picker.sourceType = UIImagePickerControllerSourceTypeCamera;
picker.mediaTypes = [NSArray arrayWithObject:(NSString *)kUTTypeImage];
picker.allowsEditing = YES;
[self presentViewController:picker animated:YES completion:nil];
}
}
}
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
// do something with this image
UIImage *imagefromcamerabutton = [info objectForKey:UIImagePickerControllerEditedImage];
// handle the case where editting was not allowed...
if (!imagefromcamerabutton) imagefromcamerabutton = [info objectForKey:UIImagePickerControllerOriginalImage];
// save to photo albumn
ALAssetsLibrary *al = [Utils defaultAssetsLibrary];
[al writeImageToSavedPhotosAlbum:[imagefromcamerabutton CGImage] metadata:nil
completionBlock:^(NSURL *assetURL, NSError *error)
{
// once we know it's saved, grab the ALAsset and store
// it in our collection for display later
ALAssetsLibraryAssetForURLResultBlock resultblock = ^(ALAsset *myasset)
{
[self.photos addObject:myasset];
[self.tableView reloadData];
};
ALAssetsLibrary *assetslibrary = [Utils defaultAssetsLibrary];
[assetslibrary assetForURL:assetURL
resultBlock:resultblock
failureBlock:nil];
}];
[self dismissImagePicker];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.photos count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
ALAsset *asset = [self.photos objectAtIndex:indexPath.row];
[cell.imageView setImage:[UIImage imageWithCGImage:[asset thumbnail]]];
[cell.textLabel setText:[NSString stringWithFormat:#"Thing %d", indexPath.row+1]];
return cell;
}
This works well, but the problem is that after the user closes the app, the cell data is erased. I would like these to be able to stay as photos which are continually associated with the table unless the user taps a button to delete it from the array. From my limited understanding of how this could be done it looks like i need to somehow implement NSUserDefaults is that right or is there a better practice to achieve this goal?
NSUserDefaults is loaded completely into memory when it is accessed. This means if you data source contains more than a few items this would be really slow or it could even crash when the phone runs out of memory.
But, when I understand you code correctly you are storing the photos in the camera roll. Therefore you have some kind of URL to the image already (the assetURL in your code).
For a quick and kind of dirty solution (which I find OK for small amounts of data which does not change often, I use NSCoding. So you can add the title and the assetURL into a NSDictionary and add the dictionary to the array of your data source. Then in dealloc call
[NSKeyedArchiver archiveRootObject:self.myDataSourceArray toFile:[self pathToFileInDocumentsDirectory]];
In viewDidLoad you can then get the data back you call:
self.myDataSourceArray = [NSKeyedUnarchiver unarchiveObjectWithFile:[self pathToFileInDocumentsDirectory]];
If the data contains many items or the items are about to change much I tend to use Core Data or Sqlite. How to use those doesn't fit into this answer.

memory crash when storing NSOperationQueues in a NSCache

I have a UITableView that loads thumbnails into cells aynchronously as follows:
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:
^{
ThumbnailButtonView *thumbnailButtonView = [tableViewCell.contentView.subviews objectAtIndex:i];
UIImage *image = [self imageAtIndex:startingThumbnailIndex + i];
[self.thumbnailsCache setObject: image forKey:[NSNumber numberWithInt:startingThumbnailIndex + i]];
[[NSOperationQueue mainQueue] addOperationWithBlock:
^{
UITableViewCell *tableViewCell = [self cellForRowAtIndexPath:indexPath];
if (tableViewCell)
{
[activityIndicatorView stopAnimating];
[self setThumbnailButtonView:thumbnailButtonView withImage:image];
}
}];
}];
[self.operationQueue addOperation:operation];
[self.operationQueues setObject:operation forKey:[NSNumber numberWithInt:startingThumbnailIndex + i]];
As per a technique I learned in a WWDC presentation, I store all of my operation queues in a NSCache called operationQueues so that later on I can cancel them if the cell scrolls off the screen (there are 3 thumbnails in a cell):
- (void) tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger startingThumbnailIndex = [indexPath row] * self.thumbnailsPerCell;
for (int i = 0; i < 3; i++)
{
NSNumber *key = [[NSNumber alloc] initWithInt:i + startingThumbnailIndex];
NSOperation *operation = [self.operationQueues objectForKey:key];
if (operation)
{
[operation cancel];
[self.operationQueues removeObjectForKey:key];
}
}
}
However, I notice if I repeatedly launch, load, then close my UITableView, I start recieving memory warnings, and then eventually the app crashes. When I remove this line:
[self.operationQueues setObject:operation forKey:[NSNumber numberWithInt:startingThumbnailIndex + i]];
The memory issues go away. Does anyone have any clue on why storing the operation queues in a cache or an array causes the app to crash?
Note: I learnt about NSCache and NSOperationQueue a couple of days ago so I might be wrong.
I don't think that this is a problem with NSOperationQueue, you are adding images to your thumbnailsCache but when the view scrolls off-screen they are still in memory. I am guessing that when the cells scroll back in, you re-create your images. This is probably clogs your memory.
Also, shouldn't you be caching your images instead of your operations?
EDIT
I did some detailed testing with NSCache by adding images and strings until my app crashed. It doesn't seem to be evicting any items so I wrote my custom cache, which seems to work:
#implementation MemoryManagedCache : NSCache
- (id)init
{
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(reduceMemoryFootprint) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
- (void)reduceMemoryFootprint
{
[self setCountLimit:self.countLimit/2];
}
#end

Resources