ios: image cache and retrieval - ios

I wish to implement caching feature for an image in iOS.
I wish to create a sample app for that, just for my understanding.
Here is what I want to do:
Get a high resolution image from internet and cache it, display it.
Now next time when this ViewController loads up, the image should be loaded from cache and not from internet.
I know how to cache an image using SDWebImage framework. I wish to try it with NSCache.
How can I implement it using NSCache? Examples are welcomed.
EDITED :
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
NSCache *cache = [self imageCache];
NSURL *url = [NSURL URLWithString:#"http://i1.ytimg.com/vi/YbT0xy_Jai0/maxresdefault.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [[UIImage alloc] initWithData:data];
NSString *imagePath = #"http://i1.ytimg.com/vi/YbT0xy_Jai0/maxresdefault.jpg";
// UIImage *image;
if (!(image = [cache objectForKey:imagePath])) { // always goes inside condition
// Cache miss, recreate image
image = [[UIImage alloc] initWithData:data];
if (image) { // Insert image in cache
[cache setObject:image forKey:imagePath]; // cache is always nil
}
}
_imgCache.image = image;

You can add image to cache using and Id:
[imageCache setObject:image forKey:#"myImage"];
Later you can retreive the image by using:
UIImage *image = [imageCache objectForKey:#"myImage"];
Entire flow will be like:
UIImage *image = [imageCache objectForKey:#"myImage"];
if (!image)
{
// download image
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:yourURL]];
if (imageData)
{
// Set image to cache
[imageCache setObject: [UIImage imageWithData:imageData] forKey:#"myImage"];
dispatch_async(dispatch_get_main_queue(), ^{
[yourImageView setImage:[UIImage imageWithData:imageData]];
});
}
});
}
else
{
// Use image from cache
[yourImageView setImage:image];
}

Refer the sample code to load image asynchronously and image caching,
https://developer.apple.com/library/ios/samplecode/LazyTableImages/Introduction/Intro.html

Related

Hide image while loading iOS

I'm picking images with UIPicker and when user picks, the download will start. I'm downloading 78 images from my server to create an animation. How can I make it while it's downloading images to hide current image(imageview), and when download finishes to show image(imageview). I tried this but it's not working.
- (void)viewDidLoad
{
[super viewDidLoad]
// Load starting image, otherwise screen is blank
self.radar_1 = [[UIImageView alloc]initWithFrame:CGRectMake(0, 65, self.view.frame.size.width, self.view.frame.size.width-70)];
radar_1.image = [UIImage animatedImageWithAnimatedGIFURL:[NSURL URLWithString:#"<|SERVER LINK|>"]];
[self.view addSubview:radar_1];
}
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
if (row == 2) {
self.radar_1 = [[UIImageView alloc] initWithFrame:CGRectMake(0, 65, self.view.frame.size.width, self.view.frame.size.width-70)];
self.radar_1.hidden = YES;
radar_1.animationImages = [NSArray arrayWithObjects:
[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:#"<|SERVER LINK|> "]]],
[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:#"<|SERVER LINK|> "]]],
[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:#"<|SERVER LINK|>"]]],
[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:#"<|SERVER LINK|>"]]],
{...} // Doing the same 77 times...
radar_1.hidden = NO;
radar_1.animationDuration = 20.0f;
radar_1.animationRepeatCount = 0;
[radar_1 startAnimating];
[self.view addSubview: radar_1];
}
Downloading 70 images from a NSURL synchronously is probably not the best idea, from a user experience standpoint. Is there a reason you cannot put all the images together in a Sprite Sheet and download a single image, and then process it on the client end to display your animation?
If you really do need to go grab 70 images, it would be best to do that asynchronously so the UI remains responsive. Here is an example of simple Async Image load that could be used:
- (void)downloadImageWithURL:(NSURL *)url completionBlock:(void (^)(BOOL succeeded, UIImage *image))completionBlock
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if ( !error )
{
UIImage *image = [[UIImage alloc] initWithData:data];
completionBlock(YES,image);
} else{
completionBlock(NO,nil);
}
}];
}
You can hold a counter variable and iterate through your image requests calling the method above. When each is complete you can tick off another successful image request. When you have them all, then you can construct that animation you are making.
While that is occurring, you can add an ActivityIndicator to your view to let the user know stuff is happening while they are waiting.
Hope that helps some.

Animated Radar Overlay MKMapView iOS

I am fetching a gif from the NOAA of the current weather radar, I store it in a UIImage and then draw it to an MKMapView, here is some of the code I am using:
image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:#"http://radar.weather.gov/ridge/Conus/RadarImg/latest_radaronly.gif"]]];
MKMapRect theMapRect = [self.overlay boundingMapRect];
CGRect theRect = [self rectForMapRect:theMapRect];
#try {
UIGraphicsBeginImageContext(image.size);
UIGraphicsPushContext(ctx);
[image drawInRect:theRect];
UIGraphicsPopContext();
UIGraphicsEndImageContext();
}
#catch (NSException *exception) {
NSLog(#"Caught an exception while drawing radar on map - %#",[exception description]);
}
#finally {
}
Does anyone know how I can animate this gif repeatedly to achieve a weather like radar you see on the news. I have found several apps that do this and am wondering how I can incorporate it into my own project.
The link you provided doesn't actually show an animated gif. It just retrieves the latest gif. One way you could do it is load the latest 10-20 gif's the NOAA has uploaded here, they seem to update it every 10 minutes, then create a UIImage for each gif and cycle through them in a UIImageview which overlays your map.
For example:
// Set images
radarGIF01 = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:#"http://radar.weather.gov/ridge/Conus/RadarImg/Conus_20150209_0108_N0Ronly.gif"]]];
radarGIF02 = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:#"http://radar.weather.gov/ridge/Conus/RadarImg/Conus_20150209_0118_N0Ronly.gif"]]];
radarGIF03 = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:#"http://radar.weather.gov/ridge/Conus/RadarImg/Conus_20150209_0128_N0Ronly.gif"]]];
radarGIF04 = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:#"http://radar.weather.gov/ridge/Conus/RadarImg/Conus_20150209_0138_N0Ronly.gif"]]];
radarGIF05 = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:#"http://radar.weather.gov/ridge/Conus/RadarImg/Conus_20150209_0148_N0Ronly.gif"]]];
// Add images to array
radarImagesArray = [[NSArray alloc]initWithObjects: radarGIF01, radarGIF02, radarGIF03, radarGIF04, radarGIF05, nil];
// Animate images in UIImageview
_radarImageView.animationImages = radarImagesArray;
_radarImageView.animationDuration = 3.0;
_radarImageView.animationRepeatCount = 0;
[_radarImageView startAnimating];

Refreshing UITableViewCell after fetching image data by dispatch_async

I'm writing app that shows TableView with entries that contains images.
I'm trying to fetch image by executing this line of code inside cellForRowAtIndexPath method:
cell.detailTextLabel.text = [artistData objectForKey:generesKey];
dispatch_async(backgroundQueue, ^{
NSURL *url_img = [NSURL URLWithString:[artistData objectForKey:pictureKey]];
NSData* data = [NSData dataWithContentsOfURL:
url_img];
cell.imageView.image = [UIImage imageWithData:data];
[self performSelectorOnMainThread:#selector(refreshCell:) withObject:cell waitUntilDone:YES];
});
After setting image I perform selector that contains:
-(void)refreshCell:(UITableViewCell*)cell{
[cell setNeedsDisplay];
[self.view setNeedsDisplay];
[self.tableViewOutlet setNeedsDisplay];
}
And image is not shown but when I click on cell or scroll entire list, images are shown. Why my View is not refreshing? Did I missed something?
It should be enough to call setNeedsLayout() on a cell.
In swift 4 it looks like this:
DispatchQueue.global().async {
let data = try? Data(contentsOf: URL(string: imageUrl)!)
DispatchQueue.main.async {
cell.imageView?.image = UIImage(data: data!)
cell.setNeedsLayout()
}
}
You'll could always reload the cell by calling [self.tableView reloadRowsAtIndexPaths#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
In order to prevent the infinite loop once you successfully download an image you'll want to cache the results. How long you cache is up to you.
NSCache *imageCache = [[NSCache alloc] init];
imageCache.name = #"My Image Cache";
UIImage *image = [imageCache objectForKey:url_img];
if (image) {
cell.imageView.image = image;
} else {
// Do your dispatch async to fetch the image.
// Once you get the image do
[imageCache setObject:[UIImage imageWithData:data] forKey:url_img];
}
You'll want the imageCache to be a property at the ViewController level. Don't create one each time incellForRowAtIndexPath
It might be related to interacting with the UI from a background queue. Try this:
dispatch_async(backgroundQueue, ^{
NSURL *url_img = [NSURL URLWithString:[artistData objectForKey:pictureKey]];
NSData* data = [NSData dataWithContentsOfURL:url_img];
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = [UIImage imageWithData:data];
});
});

NSCache holds strong pointer to UIImage instantiated with imageWithData: and does not remove from memory on unload

I have a View Controller with a property galleryCache and when an image is downloaded using GCD and imageWithData: the image is added to the cache successfully with a key. However, when the view controller is dismissed it keeps strong pointers to those downloaded images causing them not to be removed from memory. Even if I use the removeAllObjects method on the cache in viewDidDisappear: memory does not clear up.
Does anyone know why this might be?
Here is the code for the method which downloads the images.
- (void)imageForFootageSize:(FootageSize)footageSize withCompletionHandler:(void (^)(UIImage *image))completionBlock
{
if (completionBlock) {
__block UIImage *image;
// Try getting local image from disk.
//
__block NSURL *imageURL = [self localURLForFootageSize:footageSize];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
image = [UIImage imageWithData:[NSData dataWithContentsOfURL:imageURL]];
dispatch_async(dispatch_get_main_queue(), ^{
if (image) {
completionBlock(image);
} else {
//
// Otherwise try getting remote image.
//
imageURL = [self remoteURLForFootageSize:footageSize];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
dispatch_async(dispatch_get_main_queue(), ^{
image = [UIImage imageWithData:imageData];
if (image) {
//
// Save remote image to disk
//
NSURL *photoDirectoryURL = [Footage localURLForDirectory];
// Create the folder(s) where the photos are stored.
//
[[NSFileManager defaultManager] createDirectoryAtPath:[photoDirectoryURL path] withIntermediateDirectories:YES attributes:nil error:nil];
// Save photo
//
NSString *localPath = [[self localURLForFootageSize:footageSize] path];
[imageData writeToFile:localPath atomically:YES];
}
completionBlock(image);
});
});
}
});
});
}
}
Methods which use the above class method to fetch and process the UIImage in the completionHandler.
Method inside UICollectionViewCell subclass.
- (void)setPhoto:(Photo *)photo withImage:(UIImage *)image
{
[self setBackgroundColor:[UIColor blackColor]];
[self.imageView setBackgroundColor:[UIColor clearColor]];
if (photo && !image) {
[photo imageForFootageSize:[Footage footageSizeThatBestFitsRect:self.bounds]
withCompletionHandler:^(UIImage *image) {
if ([self.delegate respondsToSelector:#selector(galleryPhotoCollectionViewCell:didLoadImage:)]) {
[self.delegate galleryPhotoCollectionViewCell:self didLoadImage:image];
}
image = nil;
}];
}
[self.imageView setImage:image];
BOOL isPhotoAvailable = (BOOL)(image);
[self.imageView setHidden:!isPhotoAvailable];
[self.activityIndicatorView setHidden:isPhotoAvailable];
}
Method in UICollectionView data source delegate
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
DIGalleryPhotoCollectionViewCell *photoCell = [collectionView dequeueReusableCellWithReuseIdentifier:photoCellIdentifier forIndexPath:indexPath];
[photoCell setDelegate:self];
Footage *footage = [self footageForIndexPath:indexPath];
Photo *photo = ([footage isKindOfClass:[Photo class]]) ? (Photo *)footage : nil;
if (photo) {
//
// Photo
//
[photoCell setPhoto:photo withImage:[self.galleryCache objectForKey:photo.footageID]];
}
return photoCell;
}
Here are the other relevant methods:
- (void)galleryPhotoCollectionViewCell:(DIGalleryPhotoCollectionViewCell *)cell didLoadImage:(UIImage *)image
{
NSIndexPath *indexPath = [self.galleryCollectionView indexPathForCell:cell];
Footage *footage = [self footageForIndexPath:indexPath];
if ([footage isKindOfClass:[Footage class]]) {
Photo *photo = (Photo *)footage;
UIImage *cachedImage = [self.galleryCache objectForKey:photo.footageID];
if (!cachedImage) {
cachedImage = image;
[self.galleryCache setObject:image forKey:photo.footageID];
}
[cell setPhoto:photo withImage:image];
}
}
And also my getter method for the NSCache property galleryCache
- (NSCache *)galleryCache
{
if (!_galleryCache) {
_galleryCache = [[NSCache alloc] init];
}
return _galleryCache;
}
EDIT
Here is a snapshot of Instruments showing the retain count history of one of the NSCache once its owner (a View Controller) is dismissed.
I'm not seeing anything obvious here, though I'd suggest putting a breakpoint where you purge the cache and make sure that's actually happening like you think it is.
If you still don't find it, you can run Allocations tool in Instruments and turn on "record reference counts" (see latter part of this answer, iOS app with ARC, find who is owner of an object), and you can find out precisely where your lingering strong reference is, at which point you can tackle the remediation.
The other obvious solution is to eliminate all of this code and use a proven image caching tool, like SDWebImage which does a lot of the memory and persistent storage caching for you. It's a pretty decent implementation.
OK, so after re examining my own code and re examining properties for the billionth x n time, it turns out my error was assigning the delegate property as a 'strong' type. Lesson learned: ALWAYS set delegates as WEAK.
I will definitely have to learn more about Instruments, however.

Caching with UIImage and downloaded images

I have a class method which fetches images with a completion block. This fetched UIImage is added to an NSCache with a relevant key. This seems to work as expected, however, in the method which fetches images I am using a UIImage's imageWithData: method, which I have discovered does not cache it's data, only imageNamed: does.
I am understandably getting memory warnings because of this, how do I make sure the images loaded with UIImage's imageWithData: method are removed from memory when not needed anymore?
EDIT
Here is the code for the method which downloads the images.
- (void)imageForFootageSize:(FootageSize)footageSize withCompletionHandler:(void (^)(UIImage *image))completionBlock
{
if (completionBlock) {
__block UIImage *image;
// Try getting local image from disk.
//
__block NSURL *imageURL = [self localURLForFootageSize:footageSize];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
image = [UIImage imageWithData:[NSData dataWithContentsOfURL:imageURL]];
dispatch_async(dispatch_get_main_queue(), ^{
if (image) {
completionBlock(image);
} else {
//
// Otherwise try getting remote image.
//
imageURL = [self remoteURLForFootageSize:footageSize];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
dispatch_async(dispatch_get_main_queue(), ^{
image = [UIImage imageWithData:imageData];
if (image) {
//
// Save remote image to disk
//
NSURL *photoDirectoryURL = [Footage localURLForDirectory];
// Create the folder(s) where the photos are stored.
//
[[NSFileManager defaultManager] createDirectoryAtPath:[photoDirectoryURL path] withIntermediateDirectories:YES attributes:nil error:nil];
// Save photo
//
NSString *localPath = [[self localURLForFootageSize:footageSize] path];
[imageData writeToFile:localPath atomically:YES];
}
completionBlock(image);
});
});
}
});
});
}
}
EDIT 2
Methods which use the above class method to fetch and process the UIImage in the completionHandler.
Method inside UICollectionViewCell subclass.
- (void)setPhoto:(Photo *)photo withImage:(UIImage *)image
{
[self setBackgroundColor:[UIColor blackColor]];
[self.imageView setBackgroundColor:[UIColor clearColor]];
if (photo && !image) {
[photo imageForFootageSize:[Footage footageSizeThatBestFitsRect:self.bounds]
withCompletionHandler:^(UIImage *image) {
if ([self.delegate respondsToSelector:#selector(galleryPhotoCollectionViewCell:didLoadImage:)]) {
[self.delegate galleryPhotoCollectionViewCell:self didLoadImage:image];
}
image = nil;
}];
}
[self.imageView setImage:image];
BOOL isPhotoAvailable = (BOOL)(image);
[self.imageView setHidden:!isPhotoAvailable];
[self.activityIndicatorView setHidden:isPhotoAvailable];
}
Method in UICollectionView data source delegate
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
DIGalleryPhotoCollectionViewCell *photoCell = [collectionView dequeueReusableCellWithReuseIdentifier:photoCellIdentifier forIndexPath:indexPath];
[photoCell setDelegate:self];
Footage *footage = [self footageForIndexPath:indexPath];
Photo *photo = ([footage isKindOfClass:[Photo class]]) ? (Photo *)footage : nil;
if (photo) {
//
// Photo
//
[photoCell setPhoto:photo withImage:[self.galleryCache objectForKey:photo.footageID]];
}
return photoCell;
}
Here are the other relevant methods:
- (void)galleryPhotoCollectionViewCell:(DIGalleryPhotoCollectionViewCell *)cell didLoadImage:(UIImage *)image
{
NSIndexPath *indexPath = [self.galleryCollectionView indexPathForCell:cell];
Footage *footage = [self footageForIndexPath:indexPath];
if ([footage isKindOfClass:[Footage class]]) {
Photo *photo = (Photo *)footage;
UIImage *cachedImage = [self.galleryCache objectForKey:photo.footageID];
if (!cachedImage) {
cachedImage = image;
[self.galleryCache setObject:image forKey:photo.footageID];
}
[cell setPhoto:photo withImage:image];
}
}
And also my getter method for the NSCache property galleryCache
- (NSCache *)galleryCache
{
if (!_galleryCache) {
_galleryCache = [[NSCache alloc] init];
}
return _galleryCache;
}
Instead of rolling your own image downloading and caching solution you might be better off using SDWebImage. Then you don't have to worry about the downloading, caching or anything. SDWebImage also using disk caching so you don't have to worry about freeing memory.
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadWithURL:imageURL options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize)
{
// progression tracking code
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished)
{
if (image)
{
// do something with image
}
}];
I'm not sure but you also might have a retain cycle:
__weak typeof(self) weakSelf = self;
[photo imageForFootageSize:[Footage footageSizeThatBestFitsRect:self.bounds] withCompletionHandler:^(UIImage *image) {
if ([weakSelf.delegate respondsToSelector:#selector(galleryPhotoCollectionViewCell:didLoadImage:)])
{
[weakSelf.delegate galleryPhotoCollectionViewCell:weakSelf didLoadImage:image];
}
image = nil;
}];

Resources