How to get filesystem path to image cached with SDWebImage (iOS) - ios

I'm using SDWebImage for image caching at UICollectionView in my iOS app.
Everything is fine, however, when user scrolls fast over the collection view there is always little pause before placeholder is replaced with the cached image. I believe this is due to cache checks. For better user experience I would like cells to show the proper image instead of placeholder once an image is actually cached. This would be easy if I could get local (device) filesystem's path of the cached image and store it at the Show instance, and use it (if exists) instead of my placeholder.
There is the possibility to pass success block to the setImageWithURL method However, the only argument it gets is UIImage instance (actually in master branch there is also BOOL variable called cached being passed too). So - is there a possibility to get image's filesystem path straight from the UIImage instance? Or should I modify SDWebImage so it pass that information along to cached instance? Or is there any better way achieve a goal I described earlier?
My Show class has imageURL and here is how show cell use SDWebImage:
#import "ShowCell.h"
#import <SDWebImage/UIImageView+WebCache.h>
#implementation ShowCell
#synthesize imageView = _imageView;
#synthesize label = _label;
#synthesize show = _show;
- (void)setShow:(Show *)show
{
if (_show != show) {
self.label.text = show.title;
if (show.imageURL) {
[self.imageView setImageWithURL:[NSURL URLWithString:show.imageURL]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
}
_show = show;
}
}
#end

There are private SDImageCache methods to get filesystem path to cached image. We can make these methods public with a category. Just put the code below into the, say, SDImageCache+Private.h and add it to your project:
#import "SDImageCache.h"
#interface SDImageCache (PrivateMethods)
- (NSString *)defaultCachePathForKey:(NSString *)key;
- (NSString *)cachedFileNameForKey:(NSString *)key;
#end

I had a similar problem, see: SDWebImage showing placeholder for images in cache
Basically I forked the project to resolve this.
Update:
You can just do this to avoid the flickering (this works with the master branch of the project):
[imageView setImageWithURL:url placeholderImage:imageView.image options:0
progress:^(NSUInteger receivedSize, long long expectedSize) {
imageView.image = [UIImage imageNamed:#"QuestionMarkFace.png"];
} completed:nil];

Related

How do I get cached image stored by AFNetworking's UIImageView category?

I'm using following category UIImageView+AFNetworking.h from AFNetworking in my app, its working fine, its caching photos for me and loads images smoothly.
At one point, I want to get an image which is already there in my cache.
So I dig up into above category class where I found following code, which I think – can be helpful.
Here's the snippet from it:
#implementation AFImageCache
- (UIImage *)cachedImageForRequest:(NSURLRequest *)request {
switch ([request cachePolicy]) {
case NSURLRequestReloadIgnoringCacheData:
case NSURLRequestReloadIgnoringLocalAndRemoteCacheData:
return nil;
default:
break;
}
return [self objectForKey:AFImageCacheKeyFromURLRequest(request)];
}
- (void)cacheImage:(UIImage *)image
forRequest:(NSURLRequest *)request
{
if (image && request) {
[self setObject:image forKey:AFImageCacheKeyFromURLRequest(request)];
}
}
#end
If you want me to add web version of this, it's already here.
I think, - (UIImage *)cachedImageForRequest:(NSURLRequest *)request is the method which can return me my cached image.
But I'm not sure, how can I use it?
If I know Objective-C a bit, it's a kinda a "protocol"?
What I have tried so far is to use it like a protocol in one of my view controller. Like this:
#import <UIKit/UIKit.h>
#interface MyViewController : UIViewController<AFImageCache>
#end
But then it is giving me following warnings:
I'm not sure how to resolve those warnings and get the image from cache? Or any other more appropriate way?
I assume you import UIImageView+AFNetworking.h header.
If you want to access to the cached image. You should have a NSURLRequest object. Then it is like this:
UIImage *image = [[UIImageView sharedImageCache] cachedImageForRequest:request];
Done!

How do I correctly set up asynchronous image downloading within a UICollectionView that uses a custom cell?

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.

JMImageCache in a simple test app crashes with EXC_BAD_ACCESS

In Xcode 5.0.2
I create a blank single view app for iPhone,
then add a "male.png" image to the project,
drag a UIImageView to the storyboard
and finally add the following code to the viewDidLoad:
_imageView.image = [UIImage imageNamed:#"male.png"];
This works well:
Then I add the 4 files from JMImageCache project and change the ViewController.m to:
#import "ViewController.h"
#import "JMImageCache.h"
static NSString* const kAvatar = #"http://gravatar.com/avatar/55b3816622d935e50098bb44c17663bc.png";
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[_imageView setImageWithURL:[NSURL URLWithString:kAvatar]
placeholder:[UIImage imageNamed:#"male.png"]];
}
#end
Unfortunately, this results in app crash with the error message Thread 1: EXC_BAD_ACCESS:
At his webpage Jake Marsh (the author of JMImageCache) notes:
JMImageCache purposefully uses NSString objects instead of NSURL's to make things easier and cut down on [NSURL URLWithString:#"..."] bits everywhere. Just something to notice in case you see any strange EXC_BAD_ACCESS exceptions, make sure you're passing in NSString's and not NSURL's.
But (as an iOS programming newbie) I don't understand, what exactly does Mr. Marsh mean - since his file UIImageView+JMImageCache.m declares the 1st argument for the public method as NSURL:
- (void) setImageWithURL:(NSURL *)url placeholder:(UIImage *)placeholderImage {
[self setImageWithURL:url key:nil placeholder:placeholderImage];
}
Is the note maybe outdated and how could I fix my app?
That's a bug in JMImageCache. setImageWithURL:key:placeholder:completionBlock: calls itself, exhausting the stack.
To work around the bug, call the longer form of the method:
[_imageView setImageWithURL:[NSURL URLWithString:kAvatar]
key:nil
placeholder:[UIImage imageNamed:#"male.png"]
completionBlock:nil
failureBlock:nil];
Or, use an older version of the library (e.g. 0.4.0). Looks like the bug was introduced in 1af09be78a.

image comparison is not working after application did become active

I am using following for comparing button's background image.
if([[button currentBackgroundImage] isEqual:[UIImage imageNamed:#"image1.png"]]){
// do something
}
The code works fine when application is in active state. But when app gets back from idle state the above code doesn't work.
Any idea why this happens?
Thanks
The images don't compare after you come back from background because you're creating a new instance of that image for your comparison by using [UIImage imageNamed:#"image1.png"] (the images are compared by looking at their hash values, not by looking at the actual image contents). If you create a property for your image, when you first use imageNamed:, and use that in the comparison, it should work properly. So, I tested with this code, and it returned true when I checked after coming back from the background (I set the button's background image in IB).
#interface ViewController ()
#property (weak, nonatomic) IBOutlet UIButton *greenButton;
#property (strong,nonatomic) UIImage *greenPng;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.greenPng = [UIImage imageNamed:#"Green.png"];
}
- (IBAction)checkImages:(id)sender {
BOOL isTheSame = [self.greenButton.currentBackgroundImage isEqual:self.greenPng];
NSLog(#"The images are %#",isTheSame? #"the same" : #"different");
NSLog(#" button image hash is %d",self.greenButton.currentBackgroundImage.hash);
NSLog(#" imageNamed image hash is %d",self.greenPng.hash);
}
After Edit: I'm not sure my explanation is quite correct -- in one run of the app, you can make multiple calls to imageNamed:, and all the images that are returned will have the same hash (including the image you pick in IB, if you do it that way). I think this is due to cashing. In any case, when you come back from the background and call imageNamed: again, it returns an image with a different hash.

UIImage caching with Xcode Asset Catalogs

We all know about the mysterious behind-the-scenes caching mechanism of UIImage's imageNamed: method. In Apple's UIImage Class Reference it says:
In low-memory situations, image data may be purged from a UIImage object to free up memory on the system. This purging behavior affects only the image data stored internally by the UIImage object and not the object itself. When you attempt to draw an image whose data has been purged, the image object automatically reloads the data from its original file. This extra load step, however, may incur a small performance penalty.
In fact, image data will not be "purged from a UIImage object to free up memory on the system" as the documentation suggests, however. Instead, the app receives memory warnings until it quits "due to memory pressure".
EDIT: When using the conventional image file references in your Xcode project, the UIImage caching works fine. It's just when you transition to Asset Catalogs that the memory is never released.
I implemented a UIScrollView with a couple of UIImageViews to scroll through a long list of images. When scrolling, the next images are being loaded and assigned to the UIImageView's image property, removing the strong link to the UIImage it has been holding previously.
Because of imageNamed:'s caching mechanism, I quickly run out of memory, though, and the app terminates with around 170 MB memory allocated.
Of course there are plenty of interesting solutions around to implement custom caching mechanisms, including overriding the imageNamed: class method in a category. Often, the class method imageWithContentOfFile: that does not cache the image data is used instead, as even suggested by Apple developers at the WWDC 2011.
These solutions work fine for regular image files, although you have to get the path and file extension which is not quite as elegant as I would like it to be.
I am using the new Asset Catalogs introduced in Xcode 5, though, to make use of the mechanisms of conditionally loading images depending on the device and the efficient image file storage. As of now, there seems to be no straight forward way to load an image from an Asset Catalog without using imageNamed:, unless I am missing an obvious solution.
Do you guys have figured out a UIImage caching mechanism with Asset Catalogs?
I would like to implement a category on UIImage similar to the following:
static NSCache *_cache = nil;
#implementation UIImage (Caching)
+ (UIImage *)cachedImageNamed:(NSString *)name {
if (!_cache) _cache = [[NSCache alloc] init];
if (![_cache objectForKey:name]) {
UIImage *image = ???; // load image from Asset Catalog without internal caching mechanism
[_cache setObject:image forKey:name];
}
return [_cache objectForKey:name];
}
+ (void)emptyCache {
[_cache removeAllObjects];
}
#end
Even better would of course be a way to have more control over UIImage's internal cache and the possibility to purge image data on low memory conditions as described in the documentation when using Asset Catalogs.
Thank you for reading and I look forward to your ideas!
UPDATE: Cache eviction works fines (at least since iOS 8.3).
I am running into the same issue (iOS 7.1.1) and I kind of though that #Lukas might be right
There is a high probability that the mistake is not inside Apple's ... caching but in your .. code.
Therefore I have written a very simple Test App (view full source below) where I still see the issue. If you see anything wrong with it, please let the me know about it. I know that it really depends on the image sizes. I only see the issue on an iPad Retina.
#interface ViewController ()
#property (nonatomic, strong) UIImageView *imageView;
#property (nonatomic, strong) NSArray *imageArray;
#property (nonatomic) NSUInteger counter;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.imageArray = #[#"img1", ... , #"img568"];
self.counter = 0;
UIImage *image = [UIImage imageNamed:[self.imageArray objectAtIndex:self.counter]];
self.imageView = [[UIImageView alloc] initWithImage: image];
[self.view addSubview: self.imageView];
[self performSelector:#selector(loadNextImage) withObject:nil afterDelay:1];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
NSLog(#"WARN: %s", __PRETTY_FUNCTION__);
}
- (void)loadNextImage{
self.counter++;
if (self.counter < [self.imageArray count])
{
NSLog(#"INFO: %s - %lu - %#",
__PRETTY_FUNCTION__,
(unsigned long)self.counter,
[self.imageArray objectAtIndex:self.counter]);
UIImage *image = [UIImage imageNamed:[self.imageArray objectAtIndex:self.counter]];
self.imageView.frame = CGRectMake(0, 0, image.size.width, image.size.height);
[self.imageView setImage:image];
[self performSelector:#selector(loadNextImage) withObject:nil afterDelay:0.2];
} else
{
NSLog(#"INFO: %s %#", __PRETTY_FUNCTION__, #"finished");
[self.imageView removeFromSuperview];
}
}
#end
Inplace Implementation
I wrote some code to keep the image asset but load it with imageWithData: or imageWithContentsOfFile: use xcassets without imageNamed to prevent memory problems?

Resources