Good evening everyone, I was hoping you could help with an Objective-C question I have.
In my app, I have a mutable array that contains 16 objects; the objects being images.
I would like to save and load the array so that the images are retained when the user quits the program.
I am new to data persistence and I can see there are several ways of saving and loading data and I am familiar with the NSUserDefaults method of saving and loading data. I am aware though that you cannot save arrays with images in this way.
Could someone please explain, perhaps with an example of the best and simplest way of saving and loading an array with images? Any help would be great as I'm unsure the best way to go with this.
Thanks everyone in advance!
Consider using NSKeyedArchiver.
// Archive
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:theArray];
NSString *path = #"/Users/Anne/Desktop/archive.dat";
[data writeToFile:path options:NSDataWritingAtomic error:nil];
// Unarchive
NSString *path = #"/Users/Anne/Desktop/archive.dat";
NSMutableArray *theArray = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
This way you can be sure the unarchived array is identical to the original.
All classes conforming to the NSCoding protocol can be used by NSKeyedArchiver.
Note: You can use any extension.
Response to comment:
The following should work on iOS:
// The Array
NSMutableArray * array = [[NSMutableArray alloc] init];
// Determine Path
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *path = [ [paths objectAtIndex:0] stringByAppendingPathComponent:#"archive.dat"];
// Archive Array
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:array];
[data writeToFile:path options:NSDataWritingAtomic error:nil];
// Unarchive Array
NSMutableArray *theArray = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
Related
I've finally managed to save my data using NSCoding and a plist file.
However, the solution (cobbled together from various tutorials) uses an Archiver so the resulting plist file is binary.
I'd like to save the data in a text/readable plist xml file (at least for testing purposes) since I can read it.
How can I change my code to do this?
My "encodeWithCoder" and "initWithCoder" methods are in my Class designation.
My AppDelegate uses this to save the data where "allMsgs" is an array of Class instances:
NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:allMsgs];
[encodedObject writeToFile:plistPath atomically:YES];
The AppDelegate uses this to read the data:
NSData *codedData = [[NSData alloc] initWithContentsOfFile:plistPath];
if(codedData!=nil){
allMsgs = [NSKeyedUnarchiver unarchiveObjectWithData:codedData];
}
Here is an example on how you can save your data in a plist:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDir = [paths objectAtIndex:0];
NSString *docFilePath = [documentsDirstringByAppendingPathComponent:#"UserSettings.plist"];
NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:0];
// add the data to dict here
[dict setObject:allMsgs forKey:#"allMsgsKey"];
// save the data as plist
[dict writeToFile:docFilePath atomically:YES];
[dict release];
Since a plist is actually represented by a NSDictionary your data must be able to be stored in a dictionary.
Recently, I'm a senior in high school, and I'm interested in making apps for iPhone. Recently, one of my apps came out: NBlock. It's a puzzle app and it's very challenging. However, it has a few problems. The high scores are not saved. I've been told to use a plist. Any tips?
The URL based method for this:
// Get the URL for the document directory
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSURL *documentDirectoryURL = [[fileManager URLsForDocumentDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] objectAtIndex:0];
// Turn the filename into a string safe for use in a URL
NSString *safeString = [#"scores.plist" stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// Create an array for the score
NSMutableArray *array = [[NSMutableArray alloc] init];
[array addObject:[NSNumber numberWithInt:score]];
// Write this array to a URL
NSURL *arrayURL = [NSURL URLWithString:safeString relativeToURL:documentDirectoryURL];
[array writeToURL:arrayURL atomically:YES];
I'd avoid using a plist. The easiest way to save simple data in an application, by far, is NSUserDefaults.
Check out this tutorial for a simple guide on how to use NSUserDefaults. Always be sure to synchronize NSUserDefaults when you're done writing to them.
If you're looking for a more powerful (but more complex) way to save data, check out Apple's guide to using Core Data.
Heres what you want:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *path = [documentsDirectory stringByAppendingPathComponent:#"scores.plist"];
NSMutableArray *array = [[NSMutableArray alloc] init];
[array addObject:[NSNumber numberWithInt:score]];
[array writeToFile:path atomically:YES];
And to add new scores do initWithContentsOfFile:#"scores.plist" instead of init in the declaration of array. You can optionally use NSUserDefaults.
Take a look into NSKeyedArchiver/Unarchiver. You can save pretty much anything you want; NSUserDefaults, in my experience, dumps your data if you kill your app from the tray. Core data is really used better if you're managing large amounts of data with databases such as sqlite.
I would say the below code will work and pretty straight forward unless custom object data types(Its a different story again) are used:
NSString* plistPath = nil;
NSFileManager* manager = [NSFileManager defaultManager];
if ((plistPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:#"PathTo.plist"]))
{
if ([manager isWritableFileAtPath:plistPath])
{
NSMutableDictionary* infoDict = [NSMutableDictionary dictionaryWithContentsOfFile:plistPath];
[infoDict setObject:#"foo object" forKey:#"fookey"];
[infoDict writeToFile:plistPath atomically:NO];
[manager setAttributes:[NSDictionary dictionaryWithObject:[NSDate date] forKey:NSFileModificationDate] ofItemAtPath:[[NSBundle mainBundle] bundlePath] error:nil];
}
}
setting the date attribute might be helpful to check when is the last time score was updated.
I am trying to save an array of images to the documents folder. I managed to save an image as NSData and retrieve it using the method below, but saving an array seems to be beyond me. I've looked at several other questions that relate and it seems I'm doing everything right.
Adding the image as NSData and saving the image:
[imgsData addObject:UIImageJPEGRepresentation(img, 1.0)];
[imgsData writeToFile:dataFilePath atomically:YES];
Retrieving the data:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *path = [documentsDirectory stringByAppendingPathComponent:#"imgs.dat"];
[self setDataFilePath:path];
NSFileManager *fileManager = [NSFileManager defaultManager];
if([fileManager fileExistsAtPath:dataFilePath])
imgsData = [[NSMutableArray alloc] initWithContentsOfFile:dataFilePath];
So, writing an image as NSData using the above works, but not an array of images as NSData. It inits the array, but it has 0 objects, which isn't correct, since the array I am saving has several. Does anyone have any ideas?
First of all, you should brush up Cocoa Memory Management, the first line of code is a little bit of a worry.
For data serialisation, you may like to have a go with NSPropertyListSerialization. This class serialises arrays, dictionaries, strings, dates, numbers and data objects. It has an error reporting system, unlike the initWithContentsOfFile: methods. The method names and arguments are a bit long to fit on one line, so sometimes you may see them written with Eastern Polish Christmas Tree notation. To save your imgsData object, you can use:
NSString *errString;
NSData *serialized =
[NSPropertyListSerialization dataFromPropertyList:imgsData
format:NSPropertyListBinaryFormat_v1_0
errorDescription:&errString];
[serialized writeToFile:dataFilePath atomically:YES];
if (errString)
{
NSLog(#"%#" errString);
[errString release]; // exception to the rules
}
To read it back in, use
NSString *errString;
NSData *serialized = [NSData dataWithContentsOfFile:dataFilePath];
// we provide NULL for format because we really don't care what format it is.
// or, if you do, provide the address of an NSPropertyListFormat type.
imgsData =
[NSPropertyListSerialization propertyListFromData:serialized
mutabilityOption:NSPropertyListMutableContainers
format:NULL
errorDescription:&errString];
if (errString)
{
NSLog(#"%#" errString);
[errString release]; // exception to the rules
}
Check the contents of errString to determine what went wrong. Keep in mind that these two methods are being deprecated in favour of the dataWithPropertyList:format:options:error: and propertyListWithData:options:format:error: methods, but these were added in Mac OS X 10.6 (I'm not sure if they're available on iOS).
In my iPhone app, I have two plist files to store "Themes". One is a read-only file containing default themes, and one contains custom Themes that the user has created. I'm using plist files because it's very easy for me to read from the plist and create new Theme objects.
My plist is an array of dictionary objects.
Is there any easy way to append a new dictionary object to my plist file? Or do I need to read the file into memory, append the new dictionary object, and write it back to the filesystem?
Thanks!
With Cocoa, you need to read the file into memory, append the new dictionary object, and write it back to the filesystem. If you use an XML plist, you could pretty easily parse it and incrementally write to the file, but it'd also be quite a bit bigger, so it's unlikely to be worth it.
If rewriting the plist is taking too long, you should investigate using a database instead (perhaps via Core Data). Unless the file is huge, I doubt this will be an issue even with the iPhone's memory capacity and flash write speed.
(I copied this for those who don't want to click a link from a similar question I answered here: A question on how to Get data from plist & how should it be layout)
Here are two methods to read and write values from a plist using an NSDictionary:
- (NSMutableDictionary*)dictionaryFromPlist {
NSString *filePath = #"myPlist.plist";
NSMutableDictionary* propertyListValues = [[NSMutableDictionary alloc] initWithContentsOfFile:filePath];
return [propertyListValues autorelease];
}
- (BOOL)writeDictionaryToPlist:(NSDictionary*)plistDict{
NSString *filePath = #"myPlist.plist";
BOOL result = [plistDict writeToFile:filePath atomically:YES];
return result;
}
and then in your code block somewhere:
// Read key from plist dictionary
NSDictionary *dict = [self dictionaryFromPlist];
NSString *valueToPrint = [dict objectForKey:#"Executable file"];
NSLog(#"valueToPrint: %#", valueToPrint);
// Write key to plist dictionary
NSString *key = #"Icon File";
NSString *value = #"appIcon.png";
[dict setValue:value forKey:key];
// Write new plist to file using dictionary
[self writeDictionaryToPlist:dict];
This is how I am appending data to the plist:
NSString *filePath = [self dataFilePath];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath])
{
NSMutableArray *array = [[NSMutableArray alloc] initWithContentsOfFile:filePath];
[array addObject:countdownLabel.text];
[array writeToFile:[self dataFilePath] atomically:YES];
[array release];
}
else
{
NSArray *array = [NSArray arrayWithObject:countdownLabel.text];
[array writeToFile:filePath atomically:YES];
}
I've been trying to save a plist of a NSDictionary to my app's Documents folder. I haven't tried this on the device yet but I'd like it to work on the simulator for testing purposes. The [self createDictionaryFromChoreList] method just creates a NSDictionary from some data in another class of mine. I've pretty much copied/pasted this code from the web documents and when I go to see if the file was saved or not, I find that it isn't. Here is the method block.
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *plistName = [[NSString alloc] initWithFormat:#"%#chores.plist", self.firstName];
NSString *path = [documentsDirectory stringByAppendingPathComponent:plistName];
NSDictionary *choresDictionary = [[NSDictionary alloc] initWithDictionary:[self createDictionaryFromChoreList]];
[choresDictionary writeToFile:path atomically:YES];
Any help is greatly appreciated. Thanks in advance.
-S
You should also capture the BOOL returned by writeToFile:atomically:. That will tell you if the write succeeded or not.
Also, are you sure you are looking in the right documents folder? If you have more than one app in the simulator its easy to open the wrong app's documents folder in the Finder. I did that once and it cost me a couple of hours of frustration.
Edit01:
writeToFile:atomically: returning false explains why no file exist. The simplest explanation is that something in the dictionary is not a property list object.
From the NSDictionary docs:
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.
It just takes one non-plist object buried deep in a dictionary to prevent it from being converted to a plist.
Don't forget serialize the plist data:
Here is a snippet of code that I use for writing information to a plist
NSString *errorString;
NSData *data = [NSPropertyListSerialization dataFromPropertyList:plistDict
format:NSPropertyListXMLFormat_v1_0
errorDescription:&errorString];
[plistDict release];
if (!data) {
NSLog(#"error converting data: %#", errorString);
return NO;
}
if ([data writeToFile:[XEraseAppDelegate loadSessionPlist] atomically: YES]) {
return YES;
} else {
NSLog(#"couldn't write to new plist");
return NO;
}
This is something I whipped up really quickly and it correctly writes a plist directory of name and company to the documents directory. I have a feeling your dictionary creation method might have an issue. Try this out for yourself, then add your code and make sure it works.
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *plistDirectory = [paths objectAtIndex:0];
NSString *plistPath = [plistDirectory stringByAppendingPathComponent:#"userCompany.plist"];
NSArray *userObjects = [[NSArray alloc] initWithObjects:#"Joe", #"Smith", #"Smith Co", nil];
NSArray *userKeys = [[NSArray alloc] initWithObjects:#"First Name", #"Last Name", #"Company", nil];
NSDictionary *userSettings = [[NSDictionary alloc] initWithObjects:userObjects forKeys:userKeys];
[userSettings writeToFile:plistPath atomically:YES];
Is it correct, that the name of file your writing to is:
SOEMTHINGchores.plist?
Created via:
NSString *plistName = [[NSString alloc] initWithFormat:#"%#chores.plist", self.firstName];
Also, what is the output of:
[choresDictionary print];
Some additional info would help to debug this.
Where exactly are you looking for the file?
I have the exact same code and it works fine for me.
Just that I have to dig deep to get the file. Something like:
/Users/myUserName/Library/Application Support/iPhone Simulator/User/Applications/0E62A607-8EEB-4970-B198-81CE4BDDB7AA/Documents/data.plist
And the HEX number in the path changes with every run. So I print the file path with every run.
Insert a break point at
NSDictionary *choresDictionary = [[NSDictionary alloc] initWithDictionary:[self createDictionaryFromChoreList]];
now when you step out drag your mouse over choresDictionary and check in the tooltip that its size is not 0x0 or you can simply do an NSLog of the choresDictionary
like NSLog(#"%#",choresDictionary); I think your dictionary has 0 key key value pairs thats why you are getting null into your documents folder.
Thanks,
Madhup
I was running into this issue as well. In my case it turned out that I was using NSNumbers for keys - which is not valid.