My app ships with a .plist that looks like this:
I want the user to be able to add a custom exerciseName.
So I need to create a new .plist in the user's document folder that mimics this format. Can anyone help me with this?
I need something like this (pseudo code)
if (userData == nil)
{
then create the .plist file;
setup the .plist to mimic the format of the img above.
}
now save exerciseName appropriately.
Update:
if (exerciseArray == nil)
{
NSString *path = [[NSBundle mainBundle]pathForResource:#"data" ofType:#"plist"];
NSMutableArray *rootLevel = [[NSMutableArray alloc]initWithContentsOfFile:path];
self.exerciseArray = rootLevel;
[rootLevel release];
}
What you will want to do is load the Plist into an NSDictionary, and encode that NSDictionary back to a Plist file in your applications document folder. In your applicationDidFinishLoading: method, I would do something like this:
NSString * documentFile = [NSHomeDirectory() stringByAppendingFormat:#"/Documents/myPlist.plist"];
if (![[NSFileManager defaultManager] fileExistsAtPath:documentFile]) {
// create a copy of our resource
NSString * resPath = [[NSBundle mainBundle] pathForResource:#"myPlist" ofType:#"plist"];
// NOTE: replace #"myPlist" with the name of the file in your Resources folder.
NSDictionary * dictionary = [[NSDictionary alloc] initWithContentsOfFile:resPath];
[dictionary writeToFile:documentFile atomically:YES];
[dictionary release];
}
Then, when you want to add an item, you want to use an NSMutableDictionary to modify and save the existing plist in the app's documents directory:
- (void)addExercise {
NSMutableDictionary * changeMe = [[NSMutableDictionary alloc] initWithContentsOfFile:documentFile];
... make changes ...
[changeMe writeToFile:documentFile atomically:YES];
[changeMe release];
}
To make changes, you will need to find the sub-dictionary containing the array of exercises. Then use the setObject:forKey: method on the NSMutableDictionary to set a new array containing a new list of exercises. This might look something like this:
NSMutableArray * list = [NSMutableArray arrayWithArray:[changeMe objectForKey:#"list"]];
NSArray * exercises = [[list objectAtIndex:10] objectForKey:#"exercises"];
NSDictionary * newExercise = [NSDictionary dictionaryWithObject:#"Type a LOT" forKey:#"exerciseName"];
exercises = [exercises arrayByAddingObject:newExercise];
NSMutableDictionary * dict = [NSMutableDictionary dictionaryWithDictionary:[list objectAtIndex:10]];
[dict setObject:exercises forKey:#"exercises"];
[list replaceObjectAtIndex:10 withObject:dict];
[changeMe setObject:list forKey:#"list"];
Once you make your change, it is important to remember to write changeMe to the plist file in the documents directory.
The easiest way to read and write property lists is to use the NSArray or NSDictionary classes. Your screenshot appears to be an array at the top level, so I will use that assumption for my examples.
First you need paths to the user file and original file.
NSString *pathToUserFile; // Get a path to the file in the documents directory
NSString *pathToDefaultFile; // Get a path to the original file in the application bundle
You then attempt to load the
NSMutableArray *userData;
NSArray *temporary = [NSArray arrayWithContentsOfFile:pathToUserFile];
if(!temporary) temporary = [NSArray arrayWithContentsOfFile:pathToDefaultFile];
Since it appears that you are using multiple layers containers, I am assuming that you will need the innermost arrays and dictionaries to be mutable. The normal initialization of NSMutableArray will not do this, so you need to use CFPropertyListCreateDeepCopy with the options set to have mutable containers.
userData = (NSMutableArray *)CFPropertyListCreateDeepCopy(NULL,(CFArrayRef)temporary,kCFPropertyListMutableContainers);
You now have a mutable object representing your data. You can add objects or modify existing objects the same way you handle any array, but you can only add strings, numbers, data objects, dictionaries, arrays, and dates, since those are the only types valid in property lists.
[userData addObject:newDataObject];
Finally, you write the data out to the file in the documents directory. The writeToFile:atomically: method will attempt to write out a property list, and return YES if successful. It will fail and return NO if the file could not be written, or if the contents are not all valid property list objects.
if(![userData writeToFile:pathToUserFile atomically:YES]) {
NSLog(#"Error writing to file");
}
Related
I have problem with adding and removing items in an array inside a plist file in XCODE.
I'm able to read the array by following code:
// Path to the plist (in the application bundle) ------>>>>>
NSString *path = [[NSBundle mainBundle] pathForResource:
#"Fav" ofType:#"plist"];
// Build the array from the plist ------>>>
NSDictionary *favs = [[NSMutableDictionary alloc] initWithContentsOfFile: path];
Resepies = [favs objectForKey:#"Root"];
And this my plist structure
The senario is to let the user add and remove specific item from the array at a time.
try this code -
NSString *path = [[NSBundle mainBundle] pathForResource: #"Fav" ofType:#"plist"];
// Build the array from the plist ------>>>
NSDictionary *favs = [[NSMutableDictionary alloc] initWithContentsOfFile: path];
Resepies = [favs objectForKey:#"Root"];
[Resepies addObject:addYourObjectHere];
//then add this array into dictonary ;
[favs setObject:Resepies forKey:#"Root"];
// now write dictionary to plist
[favs writeToFile:yourPlistName atomically:YES];
You can't modify files in the application bundle, which is what your code above is attempting to do.
If the file is supposed to be modifiable, you need to move it into the documents folder first (say, on first run) and then read / write to that one subsequently. There are plenty of questions dealing with how to do this, for example : create plist and copying plist to document directory
I am currently creating a controller class for my plist.in this plist I have a root dictionary that has several types in it (Number, String and Dictionary), In my controller class I check for a plist then add it to the documents so I can read and write to it.
From here I read whats in my current plist and pass those values over to tempvars I have set up in this class.
This is what my read method looks like in my plist controller class.
-(void) readPlistData
{
// Data.plist code
// get paths from root direcory
NSArray *paths = NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, YES);
// get documents path
NSString *documentsPath = [paths objectAtIndex:0];
// get the path to our Data/plist file
NSString *plistPath = [documentsPath stringByAppendingPathComponent:#"EngineProperties.plist"];
// check to see if Data.plist exists in documents
if (![[NSFileManager defaultManager] fileExistsAtPath:plistPath])
{
// if not in documents, get property list from main bundle
plistPath = [[NSBundle mainBundle] pathForResource:#"EngineProperties" ofType:#"plist"];
}
// read property list into memory as an NSData object
NSData *plistXML = [[NSFileManager defaultManager] contentsAtPath:plistPath];
NSString *errorDesc = nil;
NSPropertyListFormat format;
// convert static property liost into dictionary object
NSDictionary *temp = (NSDictionary *)[NSPropertyListSerialization propertyListFromData:plistXML mutabilityOption:NSPropertyListMutableContainersAndLeaves format:&format errorDescription:&errorDesc];
if (!temp)
{
NSLog(#"Error reading plist: %#, format: %d", errorDesc, format);
}
// assign values
self.protocolSignature = [temp objectForKey:#"Protocol"];
self.requestNumber = [temp objectForKey:#"RequestNumber"];
//How do I add the dictionary values here?
}
The reason I put the data into variables is because latter I am going to use these values to test against checks I want to perform against my db.. making sure of things like i am receiving the correct request number etc.
UPDATE:: my idea to add them to the dictionary inside the root dictionary would be something like this. which i think is not even close but it might give you a better clue to what I am trying to do.
self.cacheValue = [temp objectForKey:#"Cache Value"];
self.manufacturers = [cacheValue objectForKey:#"Manufacturers"];
self.models = [cacheValue objectForKey:#"Model"];
self.subModels = [cacheValue objectForKey:#"SubModels"];
any help would be greatly appreciated.
I believe you want to do the following:
Define your cacheValue property in the .h as a mutable dictionary.
NSMutableDictionary *cacheValue;
Serialize the plistXml as a NSMutableDictionary:
// This is the root Dictionary
NSMutableDictionary *temp = (NSMutableDictionary *)[NSPropertyListSerialization propertyListWithData:plistXML options:NSPropertyListMutableContainersAndLeaves format:NSPropertyListXMLFormat_v1_0 error:&error];
Since everything is mutable, you can now read, update, insert, delete any part of the dictionary or its subcontents. For instance, grabbing the Mutable Dictionary "Cache Value" is just:
self.cacheValue = [temp objectForKey:#"Cache Value"];
Remember to check that the object is not nil in case there isn't a value for the key. The key needs to be exactly as it appears in the plist.
Updating a value in the Mutable Dictionary is easy:
[self.cache setValue:#"New Value" forKey:#"Sub"];
And finally, to save the changes in the root Mutable Dictionary back to the plist:
/*
The flag "atomically" specifies whether the file should be written atomically or not.
If flag is YES, the receiver is written to an auxiliary file, and then the auxiliary file is renamed to path.
If flag is NO, the dictionary is written directly to path.
The YES option guarantees that path will not be corrupted even if the system crashes during writing.
*/
[self.temp writeToFile:plistPath atomically:YES];
Hope this helps, cheers!
Currently, I have a tableView which is loaded from the following array called exerciseArray:
if (exerciseArray == nil)
{
NSString *path = [[NSBundle mainBundle]pathForResource:#"data" ofType:#"plist"];
NSMutableArray *rootLevel = [[NSMutableArray alloc]initWithContentsOfFile:path];
self.exerciseArray = rootLevel;
[rootLevel release];
}
However, I want an option to let the user add their own data which will need to be displayed in this same table view. I can either add that data to the original plist but I think it will be better to create a separate plist that will have the user added data.
So after the user adds data, the I will need to combine the 2 plist data next time. Any idea how to implement this? The user added data will need to match up with original plist structure which looks like:
The new plist should only have ExerciseName and musclename, but I need to merge the two somehow.
The user-defined Property List should be stored in a file somewhere in the Documents directory. Usually, for something like this, I would suggest creating a copy of the default Plist that ships with the app to the Documents directory, then modifying and loading this file as needed. Copying the resource to the documents directory can be done as follows:
NSString * documentsPath = [NSHomeDirectory() stringByAppendingPathComponent:#"Documents"];
NSString * docPlist = [documentsPath stringByAppendingPathComponent:#"data.plist"];
if (![[NSFileManager defaultManager] fileExistsAtPath:docPlist]) {
NSString * resPlist = [[NSBundle mainBundle]pathForResource:#"data" ofType:#"plist"];
[[NSFileManager] defaultManager] copyItemAtPath:resPlist
toPath:docPlist
error:nil];
}
NSMutableArray * root = [[NSMutableArray alloc] initWithContentsOfFile:docPlist];
The above code simply finds the path to the plist in the documents directory, and checks if it exists. If it does not, it copies the data.plist resource over to the documents directory. Once this is done, it loads the root element into a mutable array, which you can then use and modify using the standard addObject: and removeObject: methods.
Finally, to save any modified data, simply write the root array back to the plist file as follows:
[root writeToFile:docPlist atomically:YES];
The only delemma with this is, while in the process of developing the app, you may wish to change or modify some data in the data.plist resource. In order for the app to copy this newly modified Property List to the documents directory, you will need to uninstall it completely from the iOS Simulator, then re-compile and run.
Edit: In order to add an exercise to an existing muscle, just do something like this:
// find the exercise dictionary
int muscleIndex = 0;
NSMutableDictionary * muscleDict = nil;
NSString * muscleName = #"Neck";
NSString * exerciseName = #"Nod your head a billion times";
// find the right muscle
for (int i = 0; i < [root count]; i++) {
NSDictionary * muscle = [root objectAtIndex:i];
if ([[muscle objectForKey:#"muscleName"] isEqualToString:muscleName]) {
muscleIndex = i;
muscleDict = [NSMutableDictionary dictionaryWithDictionary:muscle];
}
}
NSMutableArray * exercises = [NSMutableArray arrayWithArray:[muscleDict objectForKey:#"exercises"]];
[exercises addObject:exerciseName];
[muscleDict setObject:exercises forKey:#"exercises"];
[root replaceObjectAtIndex:muscleIndex withObject:muscleDict];
...
// save root here
I will be storing a few strings (maybe 10-20). I am not sure if I should use NSUserDefaults to save them, or write them out to a plist. What is considered best practice? NSUserDefaults seems like it is less lines of code, therefore quicker to implement.
I'd like to add that these string values will be added/removed by the user.
I am assuming an array, but it will work with dictionaries too.
Userdefaults, Core Data and Plists can all be read/write but if you use a plist you need to pay attention in what dir you put it. See the plist part down below.
Core Data I think it's way too much overkill, it's just strings.
It's supposed to be used when you want to persist more complex objects.
NSUserDefaults:
It's pretty fast and easy to do, though it's supposed to store only user settings.
To write them to the userdefaults:
NSArray *stringsArray = [[NSArray alloc] arrayWithObjects: string1, string2, string3, nil];
[[NSUserDefaults standardUserDefaults] setObject:stringsArray forKey:#"MyStrings"];
[[NSUserDefaults standardUserDefaults] synchronize];
To read the from the userdefaults:
NSArray *stringsArray = [[NSUserDefaults standardUserDefaults] objectForKey:#"MyStrings"];
Plist:
If your strings are going to be modified you will need to write and read a plist but you cant't write into your app's resources.
To have a read/write plist first find the documents directory
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *stringsPlistPath = [[paths objectAtIndex:0] stringByAppendingPathComponent:#"Strings.plist"];
Create the array (I am assuming the strings are string1, ...)
NSArray *stringsArray = [[NSArray alloc] arrayWithObjects: string1, string2, string3, nil];
Write it to file
[stringsArray writeToFile:stringsPlistPath atomically:YES];
To read the plist:
Find the documents directory
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *stringsPlistPath = [[paths objectAtIndex:0] stringByAppendingPathComponent:#"Strings.plist"];
Read it in:
NSArray *stringsArray = [NSArray arrayWithContentsOfFile:stringsPlistPath];
If you are storing 10-20 strings and are looking for not too many lines of code, core data is certainly much too much overhead. I recommend going with the plist. Not a lot of code:
NSURL *plistURL = [[NSBundle mainBundle] URLForResource:#"MyStrings" withExtension:#"plist"];
NSArray *stringArray = [NSArray arrayWithContentsOfURL:plistURL];
iOS ultimately stores all NSUserDefaults data to a plist file. So it will not affect the performance if that is your concern. I personally prefer using NSUserDefaults for small data and plist for a relatively large set of data.
Note: Never store any sensitive information in NSUserDefaults as anyone can see that data.
NSUserDefaults will store the user preferences into a file into the Library/Preferences folder. In theory it serves only to store some application/user properties.
Plist file are usefull to manage a single file. If you need to manage more you should use the Coredata.
There is no restriction about the size of the plist file. Otherwise you have to be careful with plist file because when you need to save or read it the entire contents of the file will be load into memory.
Using .plist
Create a plist using Xcode
Write a value to plist
NSURL *plistURL = [[NSBundle mainBundle] URLForResource:#"settings" withExtension:#"plist"];
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfURL:plistURL];
[dict setValue:#"value" forKey:#"key"];
[dict writeToURL:plistURL atomically:YES];
Read a value from plist
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfURL:plistURL];
NSString *myValue = [dict valueForKey:#"key"];
It depends on what you want to store and why. NSUserDefaults is meant for storing user preferences. You can try to use it for other things, but you probably shouldn't.
Otherwise, if your needs are simple a plist file is pretty straightforward. You can also use core data or come up with your own file format. In general, I use plist for simple tasks and then move to core data for anything more complex.
Using a plist is a good choice for storing your strings if the strings are not just user settings that can go in NSUserDefaults. As was mentioned, when using a plist you must store your plist in the Documents directory in order to write to it, because you can't write into your own app's resources. When I first learned this, I wasn't clear on where your own app's Bundle directory was vs. where the Documents directory was, so I thought I'd post example code here that first copies a plist called "Strings.plist" (that you already have in your own app's Bundle directory) to the Documents directory, and then writes to it and reads from it.
// Make a path to the plist in the Documents directory
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *stringsPlistPathIndDoc = [[paths objectAtIndex:0] stringByAppendingPathComponent:#"Strings.plist"];
// Make a path to the plist in your app's Bundle directory
NSString *stringsPlistPathInBundle = [[NSBundle mainBundle] pathForResource:#"Strings" ofType:#".plist"];
NSFileManager *fileManager = [NSFileManager defaultManager];
// Check first that you haven't already copied this plist to the Documents directory
if (![fileManager fileExistsAtPath:stringsPlistPathIndDoc])
{
NSError *error;
// Copy the plist from the Bundle directory to the Documents directory
[fileManager copyItemAtPath:stringsPlistPathInBundle toPath:stringsPlistPathIndDoc error:&error];
}
// Write your array out to the plist in the Documents directory
NSMutableArray *stringsArray = [[NSMutableArray alloc] initWithObjects:#"string1", #"string2", #"string3", nil];
[stringsArray writeToFile:stringsPlistPathIndDoc atomically:YES];
// Later if you want to read it:
stringsArray = [[NSMutableArray alloc] initWithContentsOfFile:stringsPlistPathIndDoc];
NSUSerDefaults is indeed quick to implement, but mostly as your application grows, you want to store more and more, I went directly for plist files.
Mostly, people want to store a list of something, so here is my share on how to do this with NSDictionary. This does not require you to create a plist file first, it will be created at the first time saving something
xcode 7 beta, Swift 2.0
saving
func SaveItemFavorites(items : Array<ItemFavorite>) -> Bool
{
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true) as NSArray
let docuDir = paths.firstObject as! String
let path = docuDir.stringByAppendingPathComponent(ItemFavoritesFilePath)
let filemanager = NSFileManager.defaultManager()
let array = NSMutableArray()
for var i = 0 ; i < items.count ; i++
{
let dict = NSMutableDictionary()
let ItemCode = items[i].ItemCode as NSString
dict.setObject(ItemCode, forKey: "ItemCode")
//add any aditional..
array[i] = dict
}
let favoritesDictionary = NSDictionary(object: array, forKey: "favorites")
//check if file exists
if(!filemanager.fileExistsAtPath(path))
{
let created = filemanager.createFileAtPath(path, contents: nil, attributes: nil)
if(created)
{
let succeeded = favoritesDictionary.writeToFile(path, atomically: true)
return succeeded
}
return false
}
else
{
let succeeded = notificationDictionary.writeToFile(path, atomically: true)
return succeeded
}
}
Little note from the docs:
NSDictionary.writeToFile(path:atomically:)
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.
So whatever you set at dict.SetObject() should be one of the above mentioned types.
loading
private let ItemFavoritesFilePath = "ItemFavorites.plist"
func LoadItemFavorites() -> Array<ItemFavorite>
{
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true) as NSArray
let docuDir = paths.firstObject as! String
let path = docuDir.stringByAppendingPathComponent(ItemFavoritesFilePath)
let dict = NSDictionary(contentsOfFile: path)
let dictitems : AnyObject? = dict?.objectForKey("favorites")
var favoriteItemsList = Array<ItemFavorite>()
if let arrayitems = dictitems as? NSArray
{
for var i = 0;i<arrayitems.count;i++
{
if let itemDict = arrayitems[i] as? NSDictionary
{
let ItemCode = itemDict.objectForKey("ItemCode") as? String
//get any additional
let ItemFavorite = ItemFavorite(item: ItemCode)
favoriteItemsList.append(ItemFavorite)
}
}
}
return favoriteItemsList
}
The recommended way to persist data like this is to use Core Data. While NSUserDefaults can be used to store more or less anything it's only supposed to be used to store preferences.
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];
}