I am using https://github.com/rs/SDWebImage to load images in a UITableView.
Here is how i implemented it (simple), inside cellForRowAtIndexPath
[cell.imageView setImageWithURL:[NSURL URLWithString:[item valueForKey:#"icon"]]placeholderImage:[UIImage imageNamed:#"icon_events_default.png"]];
After images are loaded in UITableView, i scroll down, and than again up, and i receive error:EXC_BAD_ACCESS
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder
{
SDWebImageManager *manager = [SDWebImageManager sharedManager];
// Remove in progress downloader from queue
[manager cancelForDelegate:self];
UIImage *cachedImage = [manager imageWithURL:url];
if (cachedImage)
{
//EXC_BAD_ACCESS hapens here
self.image = cachedImage;
}
else
{
if (placeholder)
{
self.image = placeholder;
}
[manager downloadWithURL:url delegate:self];
}
}
Any help is really appreciated.
Did you run this code through Zombies in Instruments? That should point to the problem immediately. Just select Profile from the Product menu, Instruments will start, select the Zombie instrument, then run the test scenario which causes this problem, and you should see a zombie pop up that shows how an object is still being used even though it's no longer valid.
If I had to guess, you're UITableViewCell is not being properly retained and it's either getting released or reused too quickly before the image at the url loads.
Related
I have to reload the data in the collection view, in order to set a new cells sizes according to the new data source .
Than i have to scroll to the start, and i would like to do that with animation .
every row has 1 cell in it .
So, using this :
[self.collectionView reloadData];
[self.collectionView setContentOffset:CGPointZero animated:YES];
Whats happens is that it scrolls to the start, and reloaded, but when he comes to image 1 , than i see many images replaced fast, and it stopes on some image that is not belong to the first cell, but to other cell .
If i scroll to the start- without animation (animated:NO) its not happens .
I need that animation.
What could cause this problem ?
EDIT:
I can see a similar problem when i scroll fast ,i can see images that are changing very fast in their cells before they turned to the final image that should be loaded.
I think i solve the problem, or improved things a lot .
Well when you start downloading from the net, give your block a tag :
- (void)downloadImageWithURL:(NSURL *)url AndTag:(int)Gtag completionBlock:(void (^)(BOOL succeeded, NSData *data,int tag))completionBlock
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
int thetag=Gtag;
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if (!error)
{
completionBlock(YES, data,thetag);
}
else
{
completionBlock(NO, nil,thetag);
}
}];
}
Than, when the download complete, we will check if the cell is on screen right now , if its not, we will not load its image to the cell :
[self downloadImageWithURL:url AndTag:(int)cell.tag completionBlock:^(BOOL succeeded, NSData *data,int tag)
{
BOOL isvisible=0;
for (iPhoneCostumCell *tcell in [self.collectionView visibleCells])
{
if(tcell.tag==tag)
{
isvisible=1;
break;
}
}
I think this is a good solution , and it makes things more stable . It also makes the processor work less, because if we dont find the image, we dont continue to the NSData conversion-made in another thread .
The problem is that cells are reused,
So let's see this example
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:#"YourCellId"];
cell.image = nil;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
UIImage *image = [self imageForIndexPathRow:#(indexPath.row)];
dispatch_async(dispatch_get_main_queue(), ^{
cell.image = image;
});
});
return cell;
}
As the operations are asynchronous, scrolling fast will cause to dispatch 2 operations for 1 cell.
For example lets name some cell pointer "ImageCell-1"
Now lets see, what will be, if you start fast scrolling, and 2 operations are dispatched.
First operation loads "a.png", and download is active, you scroll fast and "ImageCell-1" is now being loaded "b.png" (but "a.png" is still loading).
So "a.png" completes download and is set to "ImageCell-1", then completes "b.png" and is set to "ImageCell-1".
(this is the issue that you see => "I can see a similar problem when i scroll fast ,i can see images that are changing very fast in their cells before they turned to the final image that should be loaded.")
But also can be situation (when you use "high level libraries" like AFNetworking, when queues are managed by them, and concurrency downloading is available) "b.png" completes download and sets it to "ImageCell-1", after that completes "a.png" download and set to "ImageCell-1". This will cause to see is "a.png" instead of "b.png".
The solution is to cancel download operation, before starting new one, if you are using AFNetworking, keep NSOperations, and call [operation cancelOperation]; before starting new download.
EDIT:
If you're using NSURLConnection, you need to switch to modern way of using NSURLSession (to support request canceling).
Here I have wrote a small method, which resolves your problem
#interface MyCell ()
#property (strong, nonatomic) NSURLSessionDataTask *dataTask;
#end
- (void)loadImageFromPath:(NSString *)aPath
availableCache:(NSCache *)aCache
{
if (self.dataTask) {
[self.dataTask cancel];
}
NSData *cache = [aCache objectForKey:aPath];
if (cache) {
self.image = [UIImage imageWithData:cache];
return;
}
self.dataTask = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:aPath]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error)
{
if (data) {
self.image = [UIImage imageWithData:data];
self.dataTask = nil;
// Add to cache
[aCache setObject:data forKey:aPath];
}
}];
[self.dataTask resume];
}
Place this method in your Cell,
And call it from cellForRow, like this
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:#"YourCellId"];
[cell loadImageFromPath:imagePath availableCache:self.cache];
return cell;
}
[UIView animateWithDuration:1.0 delay:0.0 options: UIViewAnimationOptionCurveEaseIn animations:^{
[self.collectionView setContentOffset:CGPointZero animated:NO];
} completion:nil];
}];
Try to make your own animation.
At this point I'm really fed up. It's been nearly a week now trying to solve this issue so I can move ahead. I've read multiple threads and done multiple searches in regards to my slow loading choppy UICollectionView.
I've tried to do this without any libraries as well as with SDWebImage and AFNetwork. It still doesn't fix things. Images loading isn't really a problem. The problem arrives when I scroll to cells that aren't currently showing on the screen.
As of now I've deleted all the code and all traces of any libraries and would like to get help in order to implement this properly. I've made about 2 posts on this already and this would be my third attempt coming from a different angle.
Information
My backend data is stored on Parse.com
I have access to currently loaded objects by calling [self objects]
My cellForItemAtIndex is a modified version that also returns the current object of an index.
From what I understand in my cellForItemAtIndex I need to check for an image, if there isn't one I need to download one on background thread and set it so it shows in the cell, then store a copy of it in cache so that if the associated cell goes off screen when I do scroll back to it I can use the cached image rather than downloading it again.
My custom parse collectionViewController gives me all the boiler plate code I need to get access to next set of objects, current loaded objects, pagination, pull to refresh etc. I really just need to get this collection view sorted. I never needed to do any of this with my tableview of a previous app which had much more images. It's really frustrating spending a whole day trying to solve an issue and getting no where.
This is my current collectionView cellForItemAtIndex:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object
{
static NSString *CellIdentifier = #"Cell";
VAGGarmentCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier: CellIdentifier forIndexPath:indexPath];
// check for image
// if there is a cached one use that
// if not then download one on background thread
// set my cells image view with that image
// cache image for re-use.
// PFFile *userImageFile = object[#"image"];
[[cell title] setText:[object valueForKey:#"title"]]; //title set
[[cell price] setText:[NSString stringWithFormat: #"£%#", [object valueForKey:#"price"]]]; //price set
return cell;
}
I am also using a custom collectionViewCell:
#interface VAGGarmentCell : UICollectionViewCell
#property (weak, nonatomic) IBOutlet UIImageView *imageView;
#property (weak, nonatomic) IBOutlet UITextView *title;
#property (weak, nonatomic) IBOutlet UILabel *price;
#property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
#end
If there's any more information you'd like please ask. I'd just like a clear example in code of how to do this correctly, if it still doesn't work for me then I guess there is something wrong some where within my code.
I'm going to continue reading through various threads and resources I've come across in the last few days. I can say one benefit in this experience is that I have a better understanding of threads and lazy loading but it is still very frustrated that I have made any progress with my actual app.
Incase you wondered here is my previous post: In a UICollectionView how can I preload data outside of the cellForItemAtIndexPath to use within it?
I'd either like to do this quick and manually or using the AFNetwork as that didn't cause any errors or need hacks like SDWebImage did.
Hope you can help
Kind regards.
You can make use of the internal cache used by NSURLConnection for this.
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
VAGGarmentCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"VAGGarmentCell" forIndexPath:indexPath];
//Standard code for initialisation.
NSURL *url; //The image URL goes here.
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:5.0]; //timeout can be adjusted
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
if (!connectionError)
{
UIImage *image = [UIImage imageWithData:data];
//Add image as subview here.
}
}];
.
.
return cell;
}
This is for a table view, but same concept basically. I had the same issue you were having. I had to check for a cached image, if not, retrieve it from a server. The main thing to watch out for is when you retrieve the image back, you have to update it in the collection view on the main thread. You also want to check if the cell is still visible on the screen. Here is my code as an example. teamMember is a dictionary and #"avatar" is the key which contains the URL of the user's image. TeamCommitsCell is my custom cell.
// if user has an avatar
if (![teamMember[#"avatar"] isEqualToString:#""]) {
// check for cached image, use if it exists
UIImage *cachedImage = [self.imageCache objectForKey:teamMember[#"avatar"]];
if (cachedImage) {
cell.memberImage.image = cachedImage;
}
//else retrieve the image from server
else {
NSURL *imageURL = [NSURL URLWithString:teamMember[#"avatar"]];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
// if valid data, create UIImage
if (imageData) {
UIImage *image = [UIImage imageWithData:imageData];
// if valid image, update in tableview asynch
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
TeamCommitsCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath];
// if valid cell, display image and add to cache
if (updateCell) {
updateCell.memberImage.image = image;
[self.imageCache setObject:image forKey:teamMember[#"avatar"]];
}
});
}
}
});
}
}
NSURLCache is iOS's solution to caching retrieved data, including images. In your AppDelegate, initialize the shared cache via:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:8 * 1024 * 1024
diskCapacity:20 * 1024 * 1024
diskPath:nil];
[NSURLCache setSharedURLCache:cache];
return YES;
}
-(void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
[[NSURLCache sharedURLCache] removeAllCachedResponses];
}
Then use AFNetworking's UIImageView category to set the image using:
[imageView setImageWithURL:myImagesURL placeholderImage:nil];
This has proven to load images the second time around incredibly faster. If you are worried about loading images faster for the first time, you will have to create a way to determine when and how many images you want to load ahead of time. It is very common to load data using paging. If you are using paging and still are having trouble, consider using AFNetworking's:
- (void)setImageWithURLRequest:(NSURLRequest *)urlRequest
placeholderImage:(UIImage *)placeholderImage
success:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image))success
failure:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error))failure;
This way you can create an array of UIImages and using this method to return the images for each cell before dequeuing the cell. So in this case you would have two parallel arrays; one holding your data and the other holding corresponding UIImages. Memory management will eventually get out of hand so keep that in mind. If someone scrolls quickly to the bottom of the available cells, there is honestly not much else you can do since the data depends on the network connection of the user.
After several days the issue was my images were far too large. I had to resize them and this instantly solved my issue.
I literally narrowed things down and checked my images to find they were not being resized by the method I thought was resizing them. This is why I need to get myself used to testing.
I learnt a lot about GCD and caching in the past few days but this issue could have been solved much earlier.
Am I missing something? Wouldn't allow it to be used with UIImage be more versatile than just UIImageView? You could hand the UIImage around and use it for different things, where a UIImageView can really only be added to the view.
My initial thought was what if you hand it to another class and the UIImage hasn't loaded yet, it would receive nil and not be passed anything new. But the same applies if you pass a UIImageView, wouldn't it?
AFNetworking
You're right, the AFNetworking category implementation (UIImageView+AFNetworking) is bare-bones.
But you're missing AFImageResponseSerializer, which validates and decodes images and provides a few options for handling them. (In the old 1.x version of AFNetworking, this functionality is in AFImageRequestOperation.)
SDWebImage
Similarly, you're only looking at the very basic implementation of SDWebImage. There are a LOT of options here, so I suggest you review some of the other classes, but at the most basic level, you can get a UIImage like this:
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadWithURL:imageURL
options:kNilOptions
progress:^(NSUInteger receivedSize, NSUInteger expectedSize)
{
// update a progress view
}
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished)
{
if (image)
{
// Do something with the image; cacheType tells you if it was cached or not.
}
}];
Asynchronous loading
What if you hand it to another class and the UIImage hasn't loaded yet?
Well, it's all loaded asynchronously, so the completion block wouldn't get called until the UIImage was loaded.
I have a problem with loading an image from an url to display in a table. I currently have the following code to handle the image loading in a class that extends UITableViewCell:
- (void) initWithData:(NSDictionary *) data{
NSDictionary *images = [data objectForKey:#"images"];
__block NSString *poster = [images objectForKey:#"poster"];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSURL *posterURL = [[NSURL alloc] initWithString:poster];
NSData *imageData = [NSData dataWithContentsOfURL:posterURL];
if (imageData != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
// 4. Set image in cell
self.backgroundImage.image = [UIImage imageWithData:imageData];
[self setNeedsLayout];
});
}
});
self.backgroundImage.image = [UIImage imageNamed:#"static"];
}
The initWithData method is called from the ViewController in the tableView:cellForRowAtIndexPath: delegate. Everything works as expected until i scroll. From what i read, the TableView cells are recycled and because the images are being loaded async, i get rows with wrong images. Also, the images are not cached and are loaded again whenever the cell is displayed.
Eg: Scroll to the middle and immediately scroll back up. The first cell will have the image that's corresponding to the middle cell that didn't get to finish loading.
Any help or suggestions? Thank you very much :)
First of all as the comment mentioned, I would definitely recommend using an existing framework/component to do this job.
The best candidates are probably:
https://github.com/rs/SDWebImage
https://github.com/enormego/EGOImageLoading
OR if you also want a general networking library
https://github.com/AFNetworking/AFNetworking
That said, if you still want to try it on your own, you would probably want to implement caching with an NSMutableDictionary using the indexPath as the key, and the image as the value.
Assuming you have an initialized instance variable NSMutableDictionary *imageCache
In your cellForRowAtIndexPath method, before attempting to do any image loading, you would check to see if your cache already has an image for this index by doing something like this
if(! imageCache[indexPath])
{
// do your web loading here, then once complete you do
imageCache[indexPath] = // the new loaded image
}
else
{
self.backgroundImage.image = imageCache[indexPath];
}
I'm trying to load thumbnail images from a remote site onto a UITableView. I want to do this asynchronously, and I want to implement a poorman's cache for the thumbnail images. Here's my code snippet (I'll describe the problematic behavior below):
#property (nonatomic, strong) NSMutableDictionary *thumbnailsCache;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
// ...after obtaining the cell:
NSString *thumbnailCacheKey = [NSString stringWithFormat:#"cache%d", indexPath.row];
if (![[self.thumbnailsCache allKeys] containsObject:thumbnailCacheKey]) {
// thumbnail for this row is not found in cache, so get it from remote website
__block NSData *image = nil;
dispatch_queue_t imageQueue = dispatch_queue_create("queueForCellImage", NULL);
dispatch_async(imageQueue, ^{
NSString *thumbnailURL = myCustomFunctionGetThumbnailURL:indexPath.row;
image = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:thumbnailURL]];
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = [UIImage imageWithData:image];
});
});
dispatch_release(imageQueue);
[self.thumbnailsCache setObject:image forKey:thumbnailCacheKey];
} else {
// thumbnail is in cache
NSData *image = [self.thumbnailsCache objectForKey:thumbnailCacheKey];
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = [UIImage imageWithData:image];
});
}
So here are the problematic behaviors:
When the UITableView loads, thumbnails don't show up on the initial set of cells. Only when a cell moves off screen then moves back on does the thumbnail show up.
Cache isn't working at all. From what I can tell, it fails to save the thumbnail to cache altogether. That is, this line fails:
[self.thumbnailsCache setObject:image forKey:thumbnailCacheKey];
The GCD queue is getting created/released for each cell. Furthermore, the queue name is the same every time. Is this bad practice?
I'd appreciate you guys pointing out anything you see that is wrong, or even any general approach comments. Thanks.
Update:
RESOLVED: I added a call to reloadRowsAtIndexPaths and now the thumbnail images load on initial rows that display
RESOLVED: The reason it was failing is because it was adding the image object to the dictionary before the other thread completed setting that object. I created an instance method to add object to the property dictionary, so that I can call it from inside the block, ensuring it gets added after the image object is set.
You should definitively take a look at SDWebImage.
It's exactly what your looking for.
SDWebImage is also very fast and can use multicore CPU's.
1) The reason no initial image is showing is because the cell is rendered with image = nil, and so it intelligently hides the image view.
2) Did you try moving this line inside your block ?
[self.thumbnailsCache setObject:image forKey:thumbnailCacheKey];
3) This is just a way to differentiate the queues to debug, and get info from the console, if you app crashes then you can see the name of the queue. This shouldn't be a problem you having the same name for this, since it does the same operation. You wouldn't want to use this name in another table view if you have the same logic.