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.
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 may be phrasing the question incorrectly based on my situation - sorry if that's the case.
Here's the issue: In a previous version of my app, I'm saving an NSMutableArray to a plist using NSCoding.
In a new version, I've added settings data, so I now put the previous array and the new data in an NSDictionary then use NSCoding. Works fine.
However, for this release, I'll have to determine if an existing plist is the old version (NSMutableArray) or the new one (NSDictionary).
The basic code is this:
NSData *codedData=[[NSData alloc] initWithContentsOfFile:plistPath];
//EITHER this (array): allMsgs=[NSKeyedUnarchiver unarchiveObjectWithData:codedData];
//OR this (nsdict): tmpdict=(NSDictionary *)[NSKeyedUnarchiver unarchiveObjectWithData:codedData];
How can I read the plist and determine if it's an NSDictionary or NSMutableArray without throwing errors?
Just store the unarchived object as an id, then use isKindOfClass: to check its class:
id unarchivedObject = [NSKeyedUnarchiver unarchiveObjectWithData:codedData];
if ([unarchivedObject isKindOfClass:[NSArray class]]) {
// do something
} else if ([unarchivedObject isKindOfClass:[NSDictionary class]]) {
// do something else
}
The goal is to have a singleton data controller class called FetchData.h/.m that pulls data using ObjectiveFlickr ( https://github.com/lukhnos/objectiveflickr ).
FetchData.m grabs the data with this:
OFFlickrAPIContext *context = [[OFFlickrAPIContext alloc] initWithAPIKey:YOUR_KEY sharedSecret:YOUR_SHARED_SECRET];
OFFlickrAPIRequest *request = [[OFFlickrAPIRequest alloc] initWithAPIContext:context];
// set the delegate, here we assume it's the controller that's creating the request object
[request setDelegate:self];
[request callAPIMethodWithGET:#"flickr.photos.getRecent" arguments:[NSDictionary dictionaryWithObjectsAndKeys:#"1", #"per_page", nil]]
and then implement the following delegate:
- (void)flickrAPIRequest:(OFFlickrAPIRequest *)inRequest didCompleteWithResponse:(NSDictionary *)inResponseDictionary;
Currently I have this code to save the NSDictionary as a property list to a file as an alternative to a singleton:
- (void)flickrAPIRequest:(OFFlickrAPIRequest *)inRequest didCompleteWithResponse: (NSDictionary *)inResponseDictionary{
if([inResponseDictionary writeToFile:#"inResponseDictionary.xml" atomically:TRUE])
{
NSLog(#"%#", [[NSFileManager defaultManager] fileExistsAtPath:#"inResponseDictionary.xml"]);
}
}
When I read this file back, I get Null. The file is read back as such:
NSDictionary *inResponseDictionary = [NSDictionary dictionaryWithContentsOfFile:#"inResponseDictionary.xml"];
NSDictionary *photoDict = [[inResponseDictionary valueForKeyPath:#"photos.photo"] objectAtIndex:0];
NSLog(#"%#", [photoDict count]);
Is there a better way to store this data from ObjectiveFlickr so that it can be accessed by other classes and view controllers? Or is there a better way to implement this in the View Controller.
What is in the returned NSDictionary? Are you sure they are all valid plist objects? The photo data might need to be modified (say, base 64 encoded into an array) before your write will work.
The docs for NSDictionary writeToFile: say
This method recursively validates that all the contained objects are property list objects (instances of NSData, NSDate, NSNumber, NSString, NSArray, or NSDictionary) before writing out the file, and returns NO if all the objects are not property list objects, since the resultant file would not be a valid property list.
As for the singleton aspect - will you be making more than one of these calls at a time? Is there a need to persist the data? If no & no, just keep the dictionary in memory. If you have multiple calls happening at once, you'll need another layer of abstraction (some indexing) to put each call's results in it's own unique location. And that's not happening with your current implementation.
I'm having trouble in writing mutable dictionary to a file. Here's the code that I'm writing.
I'm reading the file like below: (first time when app is ran, it won't have any data)
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString* documentsDirectory = [paths objectAtIndex:0];
self.favSaveFilePath = [[NSString alloc] initWithString:[documentsDirectory stringByAppendingPathComponent:#"Favorites.plist"]];
if([fm fileExistsAtPath:favSaveFilePath])
{
favoriteJokesDictionary = [[NSMutableDictionary alloc] initWithContentsOfFile:self.favSaveFilePath];
favoriteJokes = [[NSMutableArray alloc] initWithArray:[self.favoriteJokesDictionary objectForKey:#"FavoriteJokes"]];
return;
}
I'm adding new dictionary to array as below:
-(BOOL)addJokeToFavorite:(NSDictionary*)joke
{
BOOL success = NO;
[self.favoriteJokes addObject:joke];
success = [self.favoriteJokesDictionary writeToFile:self.favSaveFilePath atomically:YES];
return success;
}
I don't know why its not wring the dictionary to file. Can any one point me the mistake that I'm doing?
The Cocoa API is very specific about what kind of values can legally be in a dictionary when it is written out to file. One particular limitation that has been known to cause problems and which is not thoroughly discussed in the official API documentation is that all of the keys in your dictionary must be of type NSString (and your values must be one of NSData, NSDate, NSNumber, NSString, NSArray, or NSDictionary), even though the dictionary itself supports keys and values of any object type.
The reason for this restriction has to do with the fact that the dictionary gets stored as an XML document (or "property-list", in Apple-speak) by default. So I'd recommend verifying that your dictionary meets the requirements to be saved to file, and restructuring it if it doesn't.
Or, if restructuring your dictionary is not feasible, you might want to take a look at the NSKeyedArchiver class instead.
Looks like your favoriteJokesDictionary doesn't exist at all when there's no save file to initialize it from. Hence, the attempt to create that save file from the non-existing dictionary in addJokeToFavorite: doesn't do anything, either. You need to create an empty dictionary when there is no save file to begin with.
I was just typing this up when Ulrich posted the correct answer. I'll add the rest here for extra clarification.
As Ulrich pointed out, when you don't have a file, your code just skips the initialization of favoriteJokesDictionary and favoriteJokes. When you try to write out the object later, favoriteJokesDictionary is nil and so the method doesn't do anything.
You can just create an empty NSMutableDictionary if the file doesn't exist yet, and since you're retaining part of the dictionary in a separate ivar as well you should create that at the same time. Something like this would work:
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString* documentsDirectory = [paths objectAtIndex:0];
self.favSaveFilePath = [[NSString alloc] initWithString:[documentsDirectory stringByAppendingPathComponent:#"Favorites.plist"]];
if([fm fileExistsAtPath:favSaveFilePath])
{
favoriteJokesDictionary = [[NSMutableDictionary alloc] initWithContentsOfFile:self.favSaveFilePath];
favoriteJokes = [[NSMutableArray alloc] initWithArray:[self.favoriteJokesDictionary objectForKey:#"FavoriteJokes"]];
} else {
favoriteJokesDictionary = [[NSMutableDictionary alloc] init];
favoriteJokes = [[NSMutableArray alloc] init];
[favoriteJokesDictionary setObject:favoriteJokes forKey:#"JokesArrayKey"];
}
That said, I think you could probably simplify your data model there a bit (unless there much more going on that you're not showing). If you need a dictionary because you're storing more than just jokes, then what you have is fine. However, I wouldn't retain the jokes array, because if you later set a new object in the array for your JokesArrayKey, your favoriteJokes ivar is still going to be pointing to the old array.
I would just grab the object from the dictionary instead whenever you need it:
NSMutableArray *jokes = [favoriteJokesDictionary objectForKey:#"JokesArrayKey"];
Even better, if you are working with the jokes array quite a bit, just pull it out into its own file. You can write an NSArray out as a plist just like you can with an NSDictionary. If you're only using the NSDictionary as a shell to write out a .plist, you can skip it entirely.
One other thing: Assuming your favSaveFilePath property is marked "retain", you have a small memory leak in the first section. You're creating a new retained string with [[NSString alloc] initWithString:...], and then retaining it again by assigning it to your property. You probably just want:
self.favSaveFilePath = [documentsDirectory stringByAppendingPathComponent:#"Favorites.plist"];
Referring to the answer aroth gave, non-ns objects in your dictionary may be more subtle than you realise.
EG: I had an NSArray of NSDictionary objects that I was turning into NSObject subclassed items, and adding these to a NSMutableDictionary.
Even though the subclass only contained NS based objects, strings etc; the NSMutableDictionary itself wouldn't save.
The solution was to save the NSArray of NSDictionary items and turn them into my custom subclass after loading the file.
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];
}