Any way to get a Cached UIImage from my 'Documents' directory? - iphone

I know that the -imageNamed: method returns a Cached UIImage, but the problem is that my image file is stored in 'Documents', and the -imageNamed: method seems to only search the Bundle... I am currently (reluctantly) using -imageWithContentsOfFile: to get my image from 'Documents' but it is not the same...Scaling up/down a UIImageView containing the resulting image is choppy and awkward. Scaling the same UIImageView containing an image created with -imageNamed: however appears very smooth. So, again: How can I get a cached UIImage from my 'Documents' if I cannot use -imageNamed:?

I made an extension of the answer provided by rpetrich and overrode the imageName: method to add more of a drop in replacement. It searches the main bundle first and then looks in the caches directory. You could of course change the caches directory to the document directory.
#interface UIImage (CacheExtensions)
+ (UIImage *)imageNamed:(NSString *)name;
+ (void)clearCache;
#end
#import "UIImage+CacheExtensions.h"
static NSMutableDictionary *UIImageCache;
#implementation UIImage (CacheExtensions)
+ (UIImage *)imageNamed:(NSString *)name
{
id result;
if (!UIImageCache)
UIImageCache = [[NSMutableDictionary alloc] init];
else {
result = [UIImageCache objectForKey:name];
if (result) return result;
}
// First, check the main bundle for the image
NSString *imagePath = [[NSBundle mainBundle] pathForResource:name ofType:nil];
result = [UIImage imageWithContentsOfFileimagePath];
if(result) {
[UIImageCache setObject:result forKey:name];
return result;
}
// If not found, search for the image in the caches directory
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cachesImagePath = [[paths lastObject] stringByAppendingPathComponent:name];
result = [UIImage imageWithContentsOfFile:cachesImagePath];
if(result) {
[UIImageCache setObject:result forKey:name];
return result;
}
return nil;
}
+ (void)clearCache
{
[UIImageCache removeAllObjects];
}
#end

The simplest way would be an NSMutableDictionary storing the cached images and a clear cache method:
#interface UIImage (CacheExtensions)
+ (id)cachedImageWithContentsOfFile:(NSString *)path;
+ (void)clearCache;
#end
static NSMutableDictionary *UIImageCache;
#implementation UIImage (CacheExtensions)
+ (id)cachedImageWithContentsOfFile:(NSString *)path
{
id result;
if (!UIImageCache)
UIImageCache = [[NSMutableDictionary alloc] init];
else {
result = [UIImageCache objectForKey:path];
if (result)
return result;
}
result = [UIImage imageWithContentsOfFile:path];
[UIImageCache setObject:result forKey:path];
return result;
}
+ (void)clearCache
{
[UIImageCache removeAllObjects];
}
#end
Note: you should call +[UIImage clearCache] from your didReceiveMemoryWarning method. Also, clearCache will invalidate all objects in the cache, not just unused items; a UIImage subclass and more complicated caching mechanism would be required to remedy this.

You can cache UIImages yourself just as -imageNamed: does. It just loads them, and then holds onto them. You can hold onto them, too, using an NSDictionary and implement your own -imageNamed:
But I'm more concerned about the trouble you're having with scaling. How are your images getting into Documents, how are you scaling them, and have you tested the same image file stored in the bundle? I doubt that -imageNamed: has anything to do with this. I would more suspect things like the fact that the bundle has some compression applied to it (though I don't yet have a theory on why this would matter in practice), differences in the file, or differences in how the rest of the program is behaving during scaling (causing contention on the disk or CPU). Caching is unlikely related to this issue.
I'd do some profiling w/ Instruments to try to find out where the choppiness is coming from. Are you maxing out the disk, CPU, memory? What's the bottleneck?

What about writing your own image cache? You have all the pieces in place, now you just need to encapsulate it and keep a record of images you've already loaded.

Related

Cache to save images in dynamic memory iphone

I am implementing a Cache in my iOS app, that would keep images downloaded in RAM.
I did some research and found some code but most of them were for caching images to permanent storage.
I tried NSCache but couldn't work it around for my need.
The requirements are:
Limit on saving images. e.g. 100.
As the Cache limit is reached, it should remove most older image inserted before adding a new one.
I'm not sure about the exact word but I think it should be called FIFO cache (First in first out).
After some research, I did the following implementation.
static NSMutableDictionary *thumbnailImagesCache = nil;
+ (UIImage *)imageWithURL:(NSString *)_imageURL
{
if (thumbnailImagesCache == nil) {
thumbnailImagesCache = [NSMutableDictionary dictionary];
}
UIImage *image = nil;
if ((image = [thumbnailImagesCache objectForKey:_imageURL])) {
DLog(#"image found in Cache")
return image;
}
/* the image was not found in cache - object sending request for image is responsible to download image and save it to cache */
DLog(#"image not found in cache")
return nil;
}
+ (void)saveImageForURL:(UIImage *)_image URLString:(NSString *)_urlString
{
if (thumbnailImagesCache == nil) {
thumbnailImagesCache = [NSMutableDictionary dictionary];
}
if (_image && _urlString) {
DLog(#"adding image to cache")
if (thumbnailImagesCache.count > 100) {
NSArray *keys = [thumbnailImagesCache allKeys];
NSString *key0 = [keys objectAtIndex:0];
[thumbnailImagesCache removeObjectForKey:key0];
}
[thumbnailImagesCache setObject:_image forKey:_urlString];
DLog(#"images count in cache = %d", thumbnailImagesCache.count)
}
}
Now the problem is that I'm not sure weather this is the correct/efficient solution. Any one have any better idea/solution?
Your assumption about the order of the keys is certainly incorrect. The order of the keys in an NSDictionary is unspecified, the key and value at index 0 need not be the oldest one. You shall store the creation date of each image in the method where you put them in the cache dictionary.
Apart from that, the rest of the code seems valid.

iOS - I'm confused how memory is being handled here?

UIImage API Reference Document:-
initWithContentsOfFile:
Initializes and returns the image object with the contents of the specified file.
- (id)initWithContentsOfFile:(NSString *)path
Parameters
path
The path to the file. This path should include the filename extension that identifies the type of the image data.
Return Value
An initialized UIImage object, or nil if the method could not find the file or initialize the image from its contents.
Considering this scenario, suppose I have a class, it could be extension of any class. Just took UIImage for example.
#interface myImage : UIImage
{
BOOL isDefaultSet;
}
-(id)initWithDefaultImage;
#end
#implementation myImage
-(id)initWithDefaultImage
{
NSString *path = [[NSBundle mainBundle] pathForResource:#"someInvalidImage" ofType:#"png"];
idDefaultSet = YES;
return [self initWithContentsOfFile:path];
}
#end
//somewhere in other class:
NSString *path = [[NSBundle mainBundle] pathForResource:#"someInvalidImage" ofType:#"png"];
myImage *myObject = [[myImage alloc] initWithDefaultImage];
UIImage *yourObject = [[UIImage alloc] initWithContentsOfFile:path];
now here in both cases,
"alloc" gives "retainCount+1"
and if
initWithDefaultImage/initWithContentsOfFile
returned nil due to some issue - lets say (invalid file path), this memory will be leaked as
myObject/yourObject
will be set to nil even though the allocation was made before init.
I have seen many implementations for extended classes/interfaces in this manner. I'm confused how memory is being handled here? can anyone share view on this?
if [super init] returns nil, nil is returned. so the control returns from method and if (someInitializingFailed) block will never be executed and memory will be leaked as alloc is already executed before calling "initWithFoo"
if [super init] returns nil, super's init has already cleaned after itself and released the memory allocated by alloc.
From Handling Initialization Failure:
You should call the release method on self only at the point of failure. If you get nil back from an invocation of the superclass’s initializer, you should not also call release.
Usually the corresponding initializer releases self (the new object) before returning nil, as in:
- (id)initWithFoo
{
self = [super init];
if (!self) return nil;
if (someInitializingFailed) {
[self release];
return nil;
}
return self;
}
You can assume that -[UIImage initWithContentsOfFile:] is implementing the same pattern. So unless Instruments does tell you there's a leak you don't need to do any special handling in your case.
You are right, sometimes people forget to handle this leak. The allocated memory needs to be released if we cannot proceed with the initialisation.
-(id)initWithDefaultImage
{
NSString *path = [[NSBundle mainBundle] pathForResource:#"someInvalidImage" ofType:#"png"];
if (path != nil)
{
self = [super initWithContentsOfFile:path];
}
else // cannot proceed with init
{
[self release];
self = nil;
}
return self;
}

NSData memory leak

Instruments is showing that i get a memory leak right there:
-(id) copyWithZone: (NSZone *) zone
{
Layer *copy = [[Layer allocWithZone:zone]init];
NSData *imageData = [[NSData alloc]initWithData:_image];
copy.image = imageData;
[imageData release];
return copy;
}
The image property is declared as it follows:
#property (nonatomic, retain) NSData *image;
Here is a screenshot of instruments, to prove that i am not lying.
Anyone see a problem in there?
The Leaks instruments shows you where an object originated, not where it "leaked". So somewhere in your code you'll have something like this:
MyClass *obj = [otherObj copy]; // or copyWithZone:
But you're not releasing or autoreleasing obj and thus create a leak.
In Objective-C, convention tells you a method should return an autoreleased object, except for methods that start with alloc, new, copy or mutableCopy. These method must return a retained object instead and the receiver is the owner and thus responsible for releasing them.
See Memory Management Policy in Apple's memory management guide.
Here is how we solved it, following the instructions given here.
-(id) copyWithZone: (NSZone *) zone
{
Layer *copy = [[Layer allocWithZone:zone]init];
copy->_image=nil;
[copy setImage:[self image]];
return copy;
}
- (id)copyWithZone:(NSZone *)zone{
Layer *copy = [[[self class] allocWithZone: zone] init];
[copy setImage:[self image]];
return copy;
}

Adding items to NSMutableArray and saving/loading

I've used this tutorial to create an app with a table view that is populated using an NSMutableArray. Now I'd like to add the functionality to add additional items to the array and save/load them. I've customized the Fruit class to look like this:
#import <UIKit/UIKit.h>
#interface Fruit : NSObject {
NSString *name;
NSString *instructions;
NSString *explination;
NSString *imagePath;
}
#property(nonatomic,copy) NSString *name;
#property(nonatomic,copy) NSString *instructions;
#property(nonatomic,copy) NSString *explination;
#property(nonatomic,copy) NSString *imagePath;
- (id)initWithName:(NSString*)n instructions:(NSString *)inst explination:(NSString *)why imagePath:(NSString *)img;
#end
and the Fruit.m file:
#import "Fruit.h"
#implementation Fruit
#synthesize name,instructions,explination,imagePath;
- (id)initWithName: (NSString*)n instructions:(NSString*)inst explination:(NSString *)why imagePath:(NSString *)img {
self.name = n;
self.instructions = inst;
self.explination = why;
self.imagePath = img;
return self;
}
#end
and this works great, I can load two textviews and an imageView, instead of just one textview. But how would I go about saving any new items the user creates, and loading them (if they exist) when the app gets launched again?
to save your array to disk you need a couple of things.
first you need to add some methods to your fruit class so it conforms to the NSCoding protocol.
The first method is - (id)initWithCoder:(NSCoder *)aDecoder. This method will be called when you create a Fruit object from a saved archive.
Second method is - (void)encodeWithCoder:(NSCoder *)aCoder. This method is used to save your fruit in an archive.
Sounds complicated? Actually it isn't. Just a couple lines of easy to understand code.
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
self.name = [aDecoder decodeObjectForKey:#"name"];
self.instructions = [aDecoder decodeObjectForKey:#"instructions"];
self.explanation = [aDecoder decodeObjectForKey:#"explanation"];
self.imagePath = [aDecoder decodeObjectForKey:#"imagePath"];
}
return self;
}
Look at first two lines of this init method. You have to call [super init] and do a check if self is not nil in your initWithName:instructions:explination:imagePath: method too. It won't change anything in this special case, but this will definitely change in the next few classes you write. So use it all the time.
I changed this for you. And I changed the spelling error.
- (id)initWithName: (NSString*)n instructions:(NSString*)inst explination:(NSString *)why imagePath:(NSString *)img {
self = [super init];
if (self) {
self.name = n;
self.instructions = inst;
self.explanation = why;
self.imagePath = img;
}
return self;
}
and the method for encoding:
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:name forKey:#"name"];
[aCoder encodeObject:instructions forKey:#"instructions"];
[aCoder encodeObject:explanation forKey:#"explanation"];
[aCoder encodeObject:imagePath forKey:#"imagePath"];
}
It's not necessary that the key name matches the variable name. You don't need to do this. But in my opinion it adds some clarity. As long as you decode with the same name you've used for encoding you can use whatever you want.
First part is done. Next you need to load and save your NSMutableArray to a file. But to do this you need the path to the documents directory. So I created a little helper method that goes into your controller.
- (NSString *)applicationDocumentsPath {
return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
}
Then we need to load the array from disk.
NSString *path = [[self applicationDocumentsPath] stringByAppendingPathComponent:#"some.fruits"];
NSMutableArray *array = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
if (!array) {
// if it couldn't be loaded from disk create a new one
array = [NSMutableArray array];
}
then you add as much fruits as you like, and finally, to save your array to disk you need this line.
BOOL result = [NSKeyedArchiver archiveRootObject:array toFile:path];
you can check result if the archive was done without error.
I guess this should get you started. Happy coding.
You would need to persist your array on the disk and load it when the app launches.
See the answer to this question.
Than you should read about archiving : NSArchiver
You would need to implement 2 method for your Fruit class :
EncodeWithEncoder and InitWithEncoder.
than you could archive you fruits array.
Good Luck.

iPhone - User Defaults and UIImages

I've been developing an iPhone app for the last few months. Recently I wanted to up performance and cache a few of the images that are used in the UI. The images are downloaded randomly from the web by the user so I can't add specific images to the project. I'm also already using NSUserDefaults to save other info within the app.
So now I'm attempting to save a dictionary of UIImages to my NSUserDefaults object and get...
-[UIImage encodeWithCoder:]: unrecognized selector sent to instance
I then decided to subclass UIImage with a class named UISaveableImage and implement NSCoding. So now I'm at...
#implementation UISaveableImage
-(void)encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeObject:super forKey:#"image"];
}
-(id)initWithCoder:(NSCoder *)decoder
{
if (self=[super init]){
super = [decoder decodeObjectForKey:#"image"];
}
return self;
}
#end
which isn't any better than where I started. If I was able to convert an UIImage to NSData I would be good, but all I can find are function like UIImagePNGRepresentation which require me to know what type of image this was. Something that UIImage doesn't allow me to do. Thoughts? I feel like I might have wandered down the wrong path...
You don't want to store images in NSUserDefaults. They're big blobs of data, and NSUserDefaults is stored as a plist; you want to write small bits of info to it.
You should write the images to disk, and then store the filenames to defaults:
NSString *filename = myImageFilename;
[UIImagePNGRepresentation(image) writeToFile: myImageFilename atomically];
[[NSUserDefaults standardDefaults] setObject: myImageFilename forKey: #"lastImageFilename"];
Stumbling upon this a year later. I would add (in case someone else stumbles here as well) that you should store the images in the cache directory and avoid iTunes trying to back them up.
- (NSString *)pathForSearchPath:(NSSearchPathDirectory)searchPath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(searchPath, NSUserDomainMask, YES);
NSString *directoryPath = [paths objectAtIndex:0];
return directoryPath;
}
- (NSString *)cacheDirectoryPath {
return [self pathForSearchPath:NSCachesDirectory];
}