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];
}
Related
I just finished the second tutorial for iOS development. When I add a new sighting, it works fine and turns up in the list of sightings. However, when I close the iOS simulator and reopen it, only the first entry is there (it is added manually). Should there be internal memory in the iOS simulator even after it is closed and reopened?
There seems to be some memory as if I add my contact info to the contacts, it is still there when I reopen it.
If so, how do I make sure that my array in the DataController file is similarly stored on the simulator/phone so it doesn't clear itself every time I reopen the simulator?
Thanks
You need to use persistent storage if you want to save data between sessions. Options include:
You can use plist files. For example if you have NSArray *array, you can save that to a plist using writeToFile:
[array writeToFile:filename atomically:YES];
You can then read this array with:
NSArray *array = [NSArray arrayWithContentsOfFile:filename];
This technique only works with standard NSString, NSNumber, etc., objects, not custom objects like BirdSighting, though.
For custom objects like BirdSighting, you could use NSKeyedArchiver and NSKeyedUnarchiver. By the way, these are not only generally useful classes for saving data for small data sets like this, but given that it features prominently in the new iOS 6 state preservation features, it's worth familiarizing yourself with this pattern.
You can use NSUserDefaults. This is really better suited for app settings and defaults, but theoretically could be used for saving data, too.
You can use CoreData. This is the preferred iOS technology for object persistence. It's a powerful and well engineered framework (though a tad complicated) and well suited if you're dealing with more significant amounts of data.
You can use SQLite, too. See this Ray Wenderlich article on using SQLite. And once you start using SQLite, you can consider using FMDB to simplify your coding effort.
If you wanted, for example, to use the second approach, NSKeyedArchiver and NSKeyedUnarchiver, the first thing is that you might want to do is make BirdSighting conform to NSCoding, by altering the #interface declaration in BirdSighting.h to say:
#interface BirdSighting : NSObject <NSCoding>
Second, you have to write the two NSCoding methods, initWithCoder and encodeWithCoder, in BirdSighting.m that define what properties can to be loaded/saved for this object:
- (NSArray *)keysForEncoding;
{
return #[#"name", #"location", #"date"];
}
- (id) initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (self) {
for (NSString *key in [self keysForEncoding])
{
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
for (NSString *key in self.keysForEncoding)
{
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
}
Your BirdSighting can now be loaded and saved with NSKeyedUnarchiver and NSKeyedArchiver, respectively.
So, focusing on the loading of the sightings, you have to (a) tell BirdSightingDataController.m what file to look for; and (b) instruct it to read that file during initialization:
- (NSString *)filename
{
NSString *docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
return [docsPath stringByAppendingPathComponent:#"BirdSightings"];
}
- (void)initializeDefaultDataList
{
NSString *filename = [self filename];
self.masterBirdSightingList = nil;
if ([[NSFileManager defaultManager] fileExistsAtPath:filename])
{
self.masterBirdSightingList = [NSKeyedUnarchiver unarchiveObjectWithFile:filename];
}
if (!self.masterBirdSightingList)
{
NSMutableArray *sightingList = [[NSMutableArray alloc] init];
self.masterBirdSightingList = sightingList;
BirdSighting *sighting;
NSDate *today = [NSDate date];
sighting = [[BirdSighting alloc] initWithName:#"Pigeon" location:#"Everywhere" date:today];
[self addBirdSightingWithSighting:sighting];
}
}
The BirdSightingDataController.m can also define a method to save the data:
- (BOOL)save
{
return [NSKeyedArchiver archiveRootObject:self.masterBirdSightingList toFile:[self filename]];
}
You can now, for example, call this save method whenever you add a sighting, e.g.:
- (void)addBirdSightingWithSighting:(BirdSighting *)sighting
{
[self.masterBirdSightingList addObject:sighting];
[self save];
}
Personally, rather than saving it every time a user does any change in the app, I might rather just have my app delegate save it when the app goes into background or terminates (but that requires further changes, so I won't go into that now).
But hopefully this code illustrates how you can use NSKeyArchiver and NSKeyUnarchiver to save and load data. And clearly, for more complicated scenarios, I would generally encourage you to consider Core Data. But for small data sets, like this, this archiver pattern can be useful (and as I said, is worth being familiar with because the basic technology is also used in iOS 6 app state restoration).
I am trying to access the nsstring data out side the function and also outside the class.
How can I access useridStr outside the function and class? This is my code:
-(id)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSString *loginStatus = [[NSString alloc] initWithBytes:[webData mutableBytes]
length:[webData length] encoding:NSUTF8StringEncoding];
NSDictionary *loginDict = [[loginStatus JSONValue] objectForKey:#"UserDetails"];
NSArray *userId = [loginDict valueForKey: #"userid"];
NSString *useridStr = [userId lastObject];
NSLog(#"--------------....%#", useridStr);
}
NSUserDefaults *pre = [NSUserDefaults standardUserDefaults];
[pre setObject:useridStr forKey:#"useridStr"];
where you want to need string:
NSUserDefaults *pre =[NSUserDefaults standardUserDefaults];
NSString * useridStr =[pre stringForKey:#"useridStr"];
OR
In .h file of anotherView
-(id)initUserInfo:(NSString *)string;
In .m file of anotherView
-(id)initUserInfo:(NSString *)string{
if (self = [super initWithNibName:#"nextView" bundle:nil]) {
useridStr=string
}
return self;
}
In .m File of firstView
-(IBAction)btnNext_TouchUpInside:(id)sender{
nextView *second =[[nextView alloc]initUserInfo:useridStr];
[self presentModalViewController:second animated:NO];
}
What Piyush suggested is also correct. But there is also another way to achieve it.
As he suggested NSUserDefaults to save data you should keep in mind that NSUserDefaults is generally used to save data like preferences which you want it to be stored even after Application is closed by user and you want those data again when you start your application.
So if you want to save data like preferences go for NSUserDefaults. If you want your data to be available throughout your application while its running and you do not need to save them like preferences I would recommend you declare that object globally in Appdelegate file and access them whenever you need. You should not store them as NSUserDefaults because as Apple document says whatever you store in NSUserDefaults it will be saved in user's default database. So that will consume memory of your device. So in short saving everything to NSUserDefaults won't be a good idea if we consider memory managent concepts.
I would like to have a "permanent" NSDictionary in my app, in which from the beginning of the app launch, I can get access to the elements and even after I kill the app and start it again.
This NSDictionary needs to be stored tightly with the app. One way would be to just to create the NSDictionary from a Web Service data every time the app launches, then create a singleton class that would represent this NSDictionary, but I don't think this is good.
The NSDictionary will approximately hold 10-20 objects, where the key would be a NSString or NSDate and the value would be a NSArray. The NSArray would have a maximum of approximately 50 entries in it (on average probably there will only be 5-25 entries).
I am planning to use this NSDictionary as a part of a calculation that I am doing inside the delegate locationManager:didUpdateToLocation:fromLocation: each time a user moves every 50-100 meters.
Suggestions are appreciated on the best way I could do this.
Just having the NSDictionary in a singleton will not keep it between app launches, you'll need to save it to disk, and then read it from the disk when the app starts.
If you have no custom objects (subclasses you've created) in the NSDictionary, or in the NSArrays or non at all, you can use this method to save the NSDictionary is:
- (BOOL)writeToFile:(NSString *)path atomically:(BOOL)flag
and to open the dictionary from disk:
- (id)initWithContentsOfFile:(NSString *)path
However if you do have custom objects they will need to conform to the NSCoding protocol. And you'll have to use 2 different methods to save and open it:
NSCoding is a protocol, so in your header you need to add it on the end of the interface line:
#interface myClassName : NSObject <NSCoding> {
(where the only thing you should add is <NSCoding>)
Then in your implementation of your subclass, you need to add the following methods:
- (id)initWithCoder:(NSCoder *)decoder
and:
- (void)encodeWithCoder:(NSCoder *)encoder
The initWithCoder: method gets called when you want to unarchive(/open) your NSDictionary (which somewhere contains this class)
encodeWithCoder: is what gets called when your NSDictionary is archived(/saved).
You don't call either of these yourself. You need to add code in them:
- (id)initWithCoder:(NSCoder *)decoder {
if ((self = [super initWithCoder:decoder])) {
aProperty = [[decoder decodeObjectForKey:#"aProperty"] retain];
anotherProperty = [[decoder decodeObjectForKey:#"anotherProperty"] retain];
aFloat = [decoder decodeFloatForKey:#"aFloat"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
[super encodeWithCoder:encoder];
[encoder encodeObject:aProperty forKey:#"aProperty"];
[encoder encodeObject:anotherProperty forKey:#"anotherProperty"];
[encoder encodeFloat:aFloat forKey:#"aFloat"];
}
You need to have similar lines for each value you want to store (generally all the properties, [and instance variables] your class has). Note the how the float line is different to the others.
The keys can be any string you want, as long each property has it's own unique key and that they match between the two methods. I personally use the name of the property as it's just easier to understand.
when you actually want to save your NSDictionary you use:
[NSKeyedArchiver archiveRootObject:myDictionary toFile:pathToMyDictionary];
and to open the dictionary:
NSDictionary *myDictionary = [NSKeyedUnarchiver unarchiveObjectWithFile:pathToMyDictionary];
(depending on your code you may need to retain myDictionary)
To get the path to your dictionary (for both saving and opening) do:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *pathToMyDictionary = [documentsDirectory stringByAppendingPathComponent:#"myDictionary.dat"];
Hope that helps, if you have any more questions about this answer, just comment :)
It sounds like you want NSUserDefaults. That stores app preferences but can be used other bits of data as well. It is essentially a dedicated, singleton dictionary that is universally accessible and is automatically saved.
You can easily generate an NSDictionary from a plist file using [NSDictionary dictionaryWithContentsOfFile:foo]
If appropriate, the plist could be included in your app bundle, or you could grab the data from a web service on first launch and write it out to a plist that the dictionary is loaded from in the future.
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.
How can I make my application display something only when they first launch the application for the first time. Example: They open up my app, an alert comes up, saying something like, "Do you want to play the tutorial?" Then, if they close the app, then re-open it, it won't show up again.
Thanks
I'd recommend using NSUserDefaults:
- (void)openOneTime
{
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
static const NSString* kKey = #"One Time Key";
NSObject* keyValue = [defaults objectForKey:kKey];
if (keyValue == nil)
{
[self doMyOneTimeThing]; // pop a dialog, etc...
}
// Adds an object for our key which will get found the next time around,
// bypassing the above code block. The type and value of the object is
// not important; what matters more is that an object exists for that
// key at all.
[defaults setBool:YES forKey:kKey];
}
More tips on storing data persistently:
Method 1: Use the global user preferences system. You can do this, but it might be considered slightly hacky because it is designed to store user preferences, and I think this is a gray area, since the user doesn't have explicit control here. In any case, check out the docs for NSUserDefaults to find out how to do that.
Method 2: Write to a file whose existence indicates whether or not the tutorial has been viewed. You can easily create a file with an NSData object by calling its writeToFile:atomically: method. Later, you can use the NSFileManager class to check if that file exists or not.
Sample code:
- (NSString*) filename {
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString* documentsDirectory = [paths objectAtIndex:0];
return [documentsDirectory stringByAppendingPathComponent:#"notFirstTime"];
}
- (void) setNotFirstTime {
NSData* data = [[[NSData alloc] init] autorelease];
[data writeToFile:[self filename] atomically:YES];
}
- (BOOL) isNotFirstTime {
return [[NSFileManager defaultManager] fileExistsAtPath:[self filename]];
}
You could store in your property store a boolean value saying whether it's the first time or not, then check that on application start.