Saving Successfully, but not Visibly Editing plist - ios

So this is my first time trying to save data in an iOS app. I've pieced together this code from various answers on this site in order to save a high score for a game I'm making. I created a plist named saves.plist (in my Supporting Files folder) and added a row of key #"bestScore" and type Number. The test log returns that the save is successful, and everything works; however, when I go to look at the plist after, nothing seems to have changed (the value of bestScore is 0). Am I saving to a different plist that is automatically created in my code? If this is the case, what is the point of being able to create plists in Xcode, and what is the best practice to use here as far as where/how to create/store/access plists?
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.fm = [NSFileManager defaultManager];
self.destPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];//Documents directory
self.destPath = [self.destPath stringByAppendingPathComponent:#"saves.plist"];
// If the file doesn't exist in the Documents Folder, copy it.
if (![self.fm fileExistsAtPath:self.destPath]) {
NSString *sourcePath = [[NSBundle mainBundle] pathForResource:#"saves" ofType:#"plist"];
[self.fm copyItemAtPath:sourcePath toPath:self.destPath error:nil];
}
}
- (void)saveBestScore{
NSNumber *bestScoreBox = [NSNumber numberWithUnsignedLong:self.bestScore];
NSDictionary *data = #{bestScoreBox: #"bestScore"};
BOOL successful = [data writeToFile:self.destPath atomically:YES];
successful ? NSLog(#"YES") : NSLog(#"NO");
}

When you say
when I go to look at the plist after, nothing seems to have changed
(the value of bestScore is 0)
Do you mean looking at the plist in xcode project files ? You have copied the plist into a device directory and therefore you wont be able to see the change in xcode.
If you are using simulator, you can access the changed plist at:
~/Library/Application Support/iPhone Simulator/<Simulator Version>/Applications/<application>/Documents/
One easy way of storing score is to use NSUserDefault, which is a dictionary like persistence store for each application.
Set Score:
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:#(score)
forKey:#"score"];
[userDefaults synchronize];
Get Score:
int score = [[[NSUserDefaults standardUserDefaults] objectForKey:#"score"] intValue];
UPDATE:
rmaddy mentioned NSUserDefaults supports setInteger:forKey and integerForKey: Therefore you dont need to wrap the score into a NSNumber

When you write an NSDictionary to a plist using writeToFile:, the keys and values in the dictionary must follow strict rules. All keys must be NSString objects and all values must be property values (NSString, NSNumber, NSDate, NSData, etc.).
The problem you have is your dictionary has a key that is an NSNumber, not an NSString.
It appears you actually create the dictionary incorrectly. The syntax is:
#{ key : value, key : value, ... }
Change your code to:
NSDictionary *data = #{ #"bestScore" : bestScoreBox }; // key : value
Side note - your last line should be:
NSLog(#"%#", successful ? #"YES" : #"NO");
It's not good practice to use the ternary operator to run two different commands. It's meant to return one of two values.

Related

Where to store Dictionary data in iOS

I am developing iPhone app,i have one doubt
I have an NSMutableDictionary which contains data in this format
dict at 0th index:
"ProductId":"77386",
"ProductImage":"http://static.abcd.com/images/product/large/design_gallery_slide_green - Copy (2).jpg",
"Productshortname":"Apple iPhone 5c 16GB",
"categorycode":null,
"categoryid":8,
"categoryimage":"",
"categoryshortname":"",
"favorite":"0",
"price":"31500",
"productnameinUrl":"apple-iphone-5c-16gb",
"storecount":"10"
dict at 1st index:
"ProductId":"11386",
"ProductImage":"http://static.abcd.com/images/product/large/design_gallery_slide_green - Copy (2).jpg",
"Productshortname":"Apple iPhone 5s 16GB",
"categorycode":null,
"categoryid":8,
"categoryimage":"",
"categoryshortname":"",
"favorite":"1",
"price":"31500",
"productnameinUrl":"apple-iphone-5s-16gb",
"storecount":"18"
dict at 2nd index:
"ProductId":"31386",
"ProductImage":"http://static.abcd.com/images/product/large/design_gallery_slide_green - Copy (2).jpg",
"Productshortname":"Apple iPhone 4s 16GB",
"categorycode":null,
"categoryid":8,
"categoryimage":"",
"categoryshortname":"",
"favorite":"1",
"price":"31500",
"productnameinUrl":"apple-iphone-4s-16gb",
"storecount":"38"
and so on...
What i want to do is, i want to store this dictionary indexes some where in my directory and i want to fetch it after some time or even after closing and opening the app after few times.
where should i store this kind of data ? is there any storage for strong this kind of data?
Please help and thanks in advance !!
You can store the data in NSUserdefaults and can access any time and anywhere as you want
yourdict;//Your NSDictionary Object That contains the data to store
[[NSUserDefaults standardUserDefaults] setObject:yourdict forKey:#"dict"];
[[NSUserDefaults standardUserDefaults] synchronize];
At the time of retrieval of data,
dict = [[NSUserDefaults standardUserDefaults] objectForKey:#"dict"];
You've already chosen an approved answer but I'll throw my thoughts in anyway.
This information looks like it could get large.
The user defaults isn't designed for large chunks of data. It's really meant for small bits of information, such as boolean preferences etc etc, not to be treated as an easy-to-use database.
Some problems with the user defaults:
The defaults file is read and parsed when you launch your app, regardless of whether you need your information from it at that time or not. This is because other parts of your app also use it for storing their bits of info too.
The entire defaults file needs to be parsed in order for you to retrieve anything, even if you just want a single entry.
You don't choose when the defaults file is parsed. You can't do any smart threading if it becomes huge (say you put 1000 products in there)
I'd recommend either writing the dictionary to it's own plist using NSDictionary's writeToFile: and reading using initWithContentsOfFile: (this still suffers from point #2 above)
OR
Using CoreData/sqlite to write the information to a real database.
NSDictionary methods: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSDictionary_Class/Reference/Reference.html#//apple_ref/occ/instm/NSDictionary/writeToFile:atomically:
An other option (And better in my experience) is to use - NSCoder, this option is great as you can use an Object with normal properties to access your data, which make your code more readable.
You can read about it here - NSCoding / NSKeyed​Archiver by NSHipster
An here is the reference - NSCoder docs
NSDictionary has a writeToFile: method which will do it for you.
NSDicationary writeToFile:Atomically:
use NSUserDefault and save your data like this in array
Here you can use this in anyway in your application for store value of NSUserDefaults.
// --- Saving
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
// saving an NSString
[prefs setObject:#"TextToSave" forKey:#"keyToLookupString"];
// saving an NSInteger
[prefs setInteger:42 forKey:#"integerKey"];
// saving a Double
[prefs setDouble:3.1415 forKey:#"doubleKey"];
// saving a Float
[prefs setFloat:1.2345678 forKey:#"floatKey"];
// This is suggested to synch prefs, but is not needed (I didn't put it in my tut)
[prefs synchronize];
Here you can use this in anyway in your application for get value of NSUserDefaults.
// --- Retrieving
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
// getting an NSString
NSString *myString = [prefs stringForKey:#"keyToLookupString"];
// getting an NSInteger
NSInteger myInt = [prefs integerForKey:#"integerKey"];
// getting an Float
float myFloat = [prefs floatForKey:#"floatKey"];
Thanks & Cheers ..
Looks like there can be more amount of data, so the best approach is to use core data to handle this scenario.
You can check few tutorials on how to use core data - Link1 , Link2
There are advantage of using core data over NSUserDefault and file system, as those load all the data at once and you might face some issue in performance.
You can also check following links which will illustrate you performance of different mechanism used to store data - PerformanceLinks1 PerformanceLinks2
try this
NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithObjectsAndKeys:
#"audio.caf",#"pictureAudioKey",
#"audio.m4a",#"englishAudioKey",
#"audio2.m4a",#"spanishAudioKey",
#"audio3.m4a",#"frenchAudioKey",
#"audio4.m4a",#"germanAudioKey",
#"audio5.m4a",#"italianAudioKey",
#"audio6.m4a",#"chineseAudioKey",
#"image.jpg",#"photoimagekey",
#"name.txt", #"identity",
#"imagename.txt",#"numberkey",nil];
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [documentPaths objectAtIndex:0];
NSString *dictionaryPath = [documentsDirectory stringByAppendingPathComponent:[[self imageNameTextField]text]];
dictionaryPath =[dictionaryPath stringByAppendingFormat:#"dicitonary" ] ;
NSDictionary *savedDictionary = dictionary;
NSLog(#"The Save file is:%#", savedDictionary);
[savedDictionary writeToFile:dictionaryPath atomically:YES];

local memory as well as webservice

even after so many research i haven't found a solution for this question. I am currently working on a app which uses 3 view controllers for Registration with a log out button. the last view controller has the Register button which saves all the details of registration in a web service. But if the user has filled the two view forms and logs out. The two view filled forms field should be saved in the local memory and wen the user logs it again the pre filled forms should load the fields saved in internal memory just to continue the Registration for webservice. Any idea how to implement this sort of functionality
As others have said, NSUserDefaults will suffice for what you need.
NSUserDefaults *registrationInfo = [NSUserDefaults standardUserDefaults];
Guessing you have text fields with the info you need. So pull out the text and save to a key like this.
[registrationInfo setObject:self.someTextFieldName.text forKey#"firstTextField"];
After repeating this for every text field(use different key names though), call this [registrationInfo synchronize];
To pull the data out, you open the defaults again just like the first line. And to retrieve a specific key: NSString *firstTextField = [registrationInfo objectForKey:#"firstTextField"];
To make this easier, you can also put all of your strings in an array or dictionary, and then add that as an object in your defaults. Then you only have to set/get once.
If you have large amount of data to save use CoreData else you NSUserDefaults to save it.
I suggest you to use PLIST There are mainly three steps to do this.
1) Generate .plist file.
NSError *error1;
BOOL resourcesAlreadyInDocumentsDirectory;
BOOL copied1;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath1 = [documentsDirectory stringByAppendingString:#"/epub.plist"];
resourcesAlreadyInDocumentsDirectory = [fileManager fileExistsAtPath:filePath1];
if(resourcesAlreadyInDocumentsDirectory == YES) {
} else {
NSString *path1 = [[[NSBundle mainBundle] resourcePath] stringByAppendingFormat:#"/epub.plist"];
copied1 = [fileManager copyItemAtPath:path1 toPath:filePath1 error:&error1];
if (!copied1) {
NSAssert1(0, #"Failed to copy epub.plist. Error %#", [error1 localizedDescription]);
}
}
2) Try to read(open) it.
NSMutableDictionary* dict = [[NSMutableDictionary alloc] initWithContentsOfFile:filePath1];
3) write data to plist file.
[dict setObject:[NSNumber numberWithInt:value] forKey:#"value"];
[dict writeToFile:path atomically:YES];
This is a simple way to use it. I suggest to use .plist file in place of NSUserDefaults.

iOS: Read data from .plist in dictionary

I would like to retrieve data from a plist in my .app directory.
I can not figure out how to get sub-dictionary data. For instance, I would like to get the MemoriesDictionary/Memory1/Event1/EventName value.
I am able to get the MemoryCount value into iMemCount just fine with:
int iMemCount;
//Do file searching/getting for plist
NSString *plistDirectory = [[NSBundle mainBundle] pathForResource:#"memoryDetails" ofType:#"plist"];
NSLog(#"array: %#",plistDirectory);
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath: plistDirectory]) //4
{
NSLog(#"exists");
NSMutableDictionary *savedStock = [[NSMutableDictionary alloc] initWithContentsOfFile: plistDirectory];
iMemCount = [[savedStock objectForKey:#"MemoryCount"] intValue];
} else {
NSLog(#"Does Not Exist");
iMemCount = 0;
}
NSString *string = savedStock[#"MemoriesDictionary"][#"Memory1"][#"Event1"][#"EventName"];
Edit
You could also rewrite:
int iMemCount;
iMemCount = [[savedStock objectForKey:#"MemoryCount"] intValue];
as
NSInteger iMemCount;
iMemCount = [savedStock[#"MemoryCount"] integerValue];
You really should be taking into account the possibility of your iOS code running on a 64-bit processor and use the appropriate platform safe types (NSInteger, CGFloat, CFIndex, etc)
Note also that using an NSMutableDictionary here may not do what you expect: the top-level dictionary will be mutable but all the sub-objects (arrays, dictionaries) will be immutable and throw an exception if you try to access them.
In general I’d caution against doing lookups several levels deep in dictionaries, because it’s usually a sign that you’re doing something the hard way. The pattern I like to follow is create classes that can read and write themselves to dictionaries, and then when I read in a file create instances that can be queried directly.
Dealing with a bunch of mutable dictionaries with a bunch of string keys is a recipe for heartache and disaster. You lose compile-time type checking and compile-time variable name checking and readability.
Also, I don’t know if this is a contrived example file, but I wouldn’t write the count to the file explicitly—just calculate it as needed. Duplicating data leads to data being out of sync. And it seems like MemoriesDictionary really wants to be an array, if the names of the memories are inside the sub-dictionaries, and the keys are used to keep the memories in order.

Adding Items to plist from another plist on app launch

My app is a list of items that the user can rate and take notes on etc. All of these items (dictionaries that store the user ratings and notes) are stored in a plist which is moved to the documents directory when the app first loads (if the plist file already exists in documents directory it skips this step). Now I've had a few reviews and people have been saying there should be more items in the lists. Now I'm trying to figure out how to go about adding new items to the plist. If I add them to the existing plist the users will never see the new items because the app checks to see if the database already exists in the documents directory. So they will never receive the new list. So I planned on creating a new plist with items in it and appending each new item to the old plist if the item doesn't already exist (because the user can add items themselves so I don't want to have duplicate entries in case the user added an item with the same name). My dilemma is I'm not sure where exactly/how to do the plist merging. Do I do it in the App Delegate? Do I do it in the viewDidLoad of the main view? How do I stop it from checking every time the app loads if it has already added the new items to the old plist? I know there are different ways to do it. I'm just looking for the easiest and "less load heavy on the app" way of doing it.
Ok. I solved it thanks to a couple people in one of the iOS chats. So basically inside the application didFinishLaunchingWithOptions method in my AppDelegate.m I loaded both plists into 2 different NSArrays array1 and array2, I also created an NSMutableArray array3 which I loaded with the original plist that was in the users documents directory.Then I looped through them both checking to make sure I didn't add any duplicate items based on their Name_Key (because the items in the arrays are dictionaries). If the items didn't match then I would add them to the 3rd array (array3). Then I saved array3 out to the original plist file path. Then I set an integer inside the user defaults so It would only perform this task once. I didn't want the app to go through this EVERY time the user opened the app. Below is the code I used.
int haveMergedPlist = [[NSUserDefaults standardUserDefaults] integerForKey:#"merged plist"];
if (haveMergedPlist < 1){
// I used a integer rather than a BOOL because if I ever want to perform this operation again with another app update all I have to do is change the number.
// merge code here
//new plist
NSString *newPlistPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:#"NewPlist.plist"];
NSArray *newPlistArray = [[NSArray alloc]initWithContentsOfFile:newPlistPath];
//original plist in user documents directory
NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [searchPaths lastObject];
NSString *originalPlistPath = [documentsDirectory stringByAppendingPathComponent:#"originalPlist.plist"];
NSArray *originalPlistArray = [[NSMutableArray alloc]initWithContentsOfFile:originalPlistPath];
NSMutableArray *combinedPlistArray = [[NSMutableArray alloc]initWithContentsOfFile:originalPlistPath];
// Loop through both plists arrays and add if it does not match/exist.
BOOL itemExists = YES;
for (NSDictionary *newItem in newPlistArray) {
for (NSDictionary *originalItem in originalPlistArray) {
NSString * newItemNameString = [[newItem objectForKey:NAME_KEY] lowercaseString];
NSString * origItemNameString = [[originalItem objectForKey:NAME_KEY] lowercaseString];
//Compare lowercase strings and if they don't match then add them to the original plist array.
if ([newItemNameString isEqualToString:origItemNameString]) {
itemExists = YES;
break;
} else {
itemExists = NO;
//doesn't match so add it
}
}
if (itemExists == NO) {
[combinedPlistArray addObject:newItem];
NSLog(#"newItem added");
itemExists = YES;
}
}
//Write Array With New Items Added to documents directory
NSString *writeableDBPath = [documentsDirectory stringByAppendingPathComponent:#"originalPlist.plist"];
[combinedPlistArray writeToFile:writeableDBPath atomically:YES];
//set integer to 1 in user defaults so it only merge's once
//I comment this out while getting the loops to work otherwise you will have to delete the app out of the simulator or the phone to make sure its working every time.
[[NSUserDefaults standardUserDefaults] setInteger:1 forKey:#"merged plist"];
}

Save game level in my app even when the app is closed?

I am making an RPG game for the iPhone and everything is working out great but I need to know how to save my game level so that even if the user were to close the app running in the background the entire game wouldn't start over again. I was even thinking of bringing back old style gaming and making it so that you have to enter a password to start from where you left off. But even then I wouldn't know how to save the game properly. Plus even if I did save the game how would I be able to make it stay saved even when the app closes completely? So far I have tried adding save data code to the AppWillTerminate line but still nothing. Any help is appreciated.
I'm not sure if you want to save which level the user was on, or if you want to save the game state. If you simply want to save which level the user was on, you should go with #EricS's method (NSUserDefaults). It's a little more complicated to save game state. I would do something like this:
//Writing game state to file
//Some sample data
int lives = player.kLives;
int enemiesKilled = player.kEnemiesKilled;
int ammo = player.currentAmmo;
//Storing the sample data in an array
NSArray *gameState = [[NSArray alloc] initWithObjects: [NSNumber numberWithInt:lives], [NSNumber numberWithInt:enemiesKilled], [NSNumber numberWithInt:ammo], nil];
//Writing the array to a .plist file located at "path"
if([gameState writeToFile:path atomically:YES]) {
NSLog(#"Success!");
}
//Reading from file
//Reads the array stored in a .plist located at "path"
NSArray *lastGameState = [NSArray arrayWithContentsOfFile:path];
The .plist would look like this:
Using an array would mean that upon reloading the game state, you would have to know the order that you stored the items in, which isn't that bad, but if you want a more reliable method, you could try using an NSDictionary like this:
//Writing game state to file
//Some sample data
int lives = player.kLives;
int enemiesKilled = player.kEnemiesKilled;
int ammo = player.currentAmmo;
int points = player.currentPoints;
//Store the sample data objects in an array
NSArray *gameStateObjects = [NSArray arrayWithObjects:[NSNumber numberWithInt:lives], [NSNumber numberWithInt:enemiesKilled], [NSNumber numberWithInt:points], [NSNumber numberWithInt:ammo], nil];
//Store their keys in a separate array
NSArray *gameStateKeys = [NSArray arrayWithObjects:#"lives", #"enemiesKilled", #"points", #"ammo", nil];
//Storing the objects and keys in a dictionary
NSDictionary *gameStateDict = [NSDictionary dictionaryWithObjects:gameStateObjects forKeys:gameStateKeys];
//Write to file
[gameStateDict writeToFile:path atomically: YES];
//Reading from file
//Reads the array stored in a .plist located at "path"
NSDictionary *lastGameState = [NSDictionary dictionaryWithContentsOfFile:path];
The dictionary .plist would look like this:
To save the level:
[[NSUserDefaults standardUserDefaults] setInteger:5 forKey:#"level"];
To read the level:
NSInteger level = [[NSUserDefaults standardUserDefaults] integerForKey:#"level"];
I would set it whenever the user enters that level. You could wait until you are sent into the background, but there's really no point in waiting.

Resources