I'm working on a game that requires the use of a plist. I set up the plist correctly, or at least I hope, as I've been using this method to set up plists all of the time. The problem though, is that in this case it is like the plist is not even recognized. Here are the contents of my plist:
The 'yo' key and value are debug values that I used to see if the plist was even being recognized. Here is my code:
-(NSString *)docsDir{
return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
}
-(void)UpdatePlist{
listPath = [[self docsDir] stringByAppendingPathComponent:#"details.plist"];
self.savedData = [NSMutableDictionary dictionaryWithContentsOfFile:listPath];
unsigned long storedHighScore = [[self.savedData valueForKey:#"High Score"] unsignedLongValue];
NSLog(#"%#",[[self.savedData valueForKey:#"yo"] stringValue]);
NSLog(#"Here");
if (score>storedHighScore) {
[self.savedData setValue:[NSNumber numberWithUnsignedLong:score] forKey:#"High Score"];
NSLog(#"YO %lu",[[self.savedData valueForKey:#"High Score"] unsignedLongValue]);
}
[self.savedData writeToFile:listPath atomically:YES];
}
The problem stems from the code or my declaration of the plist I'm assuming. All of the NSLogs execute, as expected, but my log file is even more interesting:
Even the test string does not get returned and gets treated as null. At this point I'm assuming something is wrong with the plist, and yes, I can assure you that the name of the plist is indeed details.plist.
Where is this plist? When/how is it initially written to the documents directory?
The documents directory is the one into which files are copied from iTunes. So if you put this file into your app's Resources, it will not end up in documents.
When the plist doesn't exist, -dictionaryWithContentsOfFile: will return nil. Calling -objectForKey: or -stringValue on nil returns nil again. So if the file doesn't exist, self.savedData will be set to nil. Calling -writeToFile:atomically: on nil also is a No-op. So the code you posted will never create the plist if it is not already there.
Have you tried stepping through this code in the debugger? Click the gutter to the left of the -dictionaryWithContentsOfFile:-line and see what self.savedData is set to?
Related
I have an iOS app in the app store that can download relatively large files that need to stay on the device for offline use. Those files are currently stored in the app's Documents folder but I'm just now reading that the Documents folder is backed up and should really only be used for user-generated content. This Apple technical Q&A states that the NSURLIsExcludedFromBackupKey should be set to prevent backup. This states that an app's /Library/Caches is the right place to put these kinds of files although further reading suggests that the folder may be cleared when the device is low on storage which is unacceptable for this app. I believe /Library/Application Support/ is then the best location for them -- does this sound right?
Unfortunately, this mistake got through the app review process. What are some best practices for fixing this now that people are using the app and already have some files persisted to the Documents folder and to their backups? It seems I need to move all the existing files and set their NSURLIsExcludedFromBackupKey on app update. How do I guarantee that this is done exactly once and that it isn't interrupted? Is moving the files out of the Documents folder important or could I leave them there? Will changing the files' backup status remove them from existing backups?
I'm using Swift 2.1.1 and targeting iOS 8.0+.
As stated in the technical Q&A, you best bet could be create a subdirectory in the Documents, and exclude that subdirectory once.
I don't believe you can write a 'do it once and be sure it is done' routine, since you can't guarantee your app doesn't crash while it is running. You certainly could set a completion flag when you are sure it is done so that once it is done you don't have to run it again.
Exclude your directory from backup, not the individual files.
From Xcode:
You can use this property to exclude cache and other app support files which are not needed in a backup. Some operations commonly made to user documents cause this property to be reset to false; consequently, do not use this property on user documents.
Here is the strategy I have used with good results
(sorry, its in objective-c -- I'm not a swift guy. Hopefully it will give you the idea):
- (BOOL)moveAllFiles{
// Searches through the documents directory for all files ending in .data
BOOL success = true;
NSString *myNewLocation = #"/store/my/files/here/now";
// Get the documents directory
NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = [documentDirectories objectAtIndex:0];
// Get all files ending in .data (whatever your file type is)
NSArray *dataFilesArray = [NSArray arrayWithArray:[NSBundle pathsForResourcesOfType:#"data" inDirectory:documentDirectory]];
// If you have multiple resource types, use this line once each for each resource type, then append the arrays.
// Iterate the found files
NSString *fileName = [NSString string];
for (int i=0; i<[dataFilesArray count]; i++) {
fileName = [[dataFilesArray objectAtIndex:i] lastPathComponent];
// Move file, set success to false if unsuccessful move
if (![[NSFileManager defaultManager] moveItemAtPath:[dataFilesArray objectAtIndex:i]
toPath:[myNewLocation stringByAppendingPathComponent:fileName]
error:nil]) {
success = false; // Something went wrong
}
}
return success;
}
Now use the value of success to set a key in the user defaults file. Check for that key on startup. If it is false (or absent), run this routine (again).
This example is with file paths. You can do the same thing with file URLs if you wish.
I have created a plist on XCode that will have a few values that I can't insert manually. So I want to add this values programmatically at development time. But it seems that I can only read the plist I can not save a plist that is on the App bundle, which makes sense at runtime.. When I will distribute my app I want everyone to have this plist file that's why I am not saving on documents or cache. How can I achieve what I want?
From http://www.karelia.com/cocoa_legacy/Foundation_Categories/NSFileManager__Get_.m (pasted below) you can build a path within the user's personal library with the -(NSString *) pathFromUserLibraryPath:(NSString *)inSubPath method found there.
For example, NSString *editedPlist = [self pathFromUserLibraryPath:#"my.plist"]; gets you the name of the modified plist within the user's library (even if that plist doesn't exist yet).
How you read/write it is according to what kind of plist you have, but you could read it into a dictionary with:
NSMutableDictionary *thePlist= [[NSMutableDictionary alloc] initWithContentsOfFile:editedPlist ];
If you are unable to read, easily detected by, for example [thePlist count] == 0, then you would instead call the same initWithContentsOfFile: initializer with a path to the template within your bundle, but you would then write the plist out to the editedPlist path so it appears in the user directory.
Here is the utility method I referenced above:
/*
NSFileManager: Get the path within the user's Library directory
Original Source: <http://cocoa.karelia.com/Foundation_Categories/NSFileManager__Get_.m>
(See copyright notice at <http://cocoa.karelia.com>)
*/
/*" Return the path in the user library path of the given sub-path. In other words, if given inSubPath is "foo", the path returned will be /Users/myUser/Library/foo
"*/
- (NSString *) pathFromUserLibraryPath:(NSString *)inSubPath
{
NSArray *domains = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask,YES);
NSString *baseDir= [domains objectAtIndex:0];
NSString *result = [baseDir stringByAppendingPathComponent:inSubPath];
return result;
}
What I would suggest is writing code that checks for the plist in the documents directory at start. If it's there, read it into memory.
If you don't find the file in the documents directory, read it from the app bundle instead. Then drop into the code that uses it from memory and writes the changed version to the documents directory.
Remember that all the objects you read from a plist file are read as immutable, even if you wrote mutable objects into the file. You have to write code that makes mutable copies of anything that you want to change. (And have to implement a mutable deep copy if you have complex structures like arrays of dictionaries that in turn contain arrays of strings.)
I have a method:
#define ROOT_ASSETS_PATH #"/assets/"
...
-(NSMutableDictionary*) getContentsOfPlist{
NSString *pageContentPlistPath = [[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: ROOT_ASSETS_PATH] stringByAppendingPathComponent:#"content.plist"];
NSMutableDictionary *mDictionary = [NSMutableDictionary dictionaryWithContentsOfFile:pageContentPlistPath];
return mDicitonary;
}
On the simulator. this returns a dictionary. On the iPad, it returns nil. Made sure the extension case matches that in the call as well.
Any ideas?
The issue was case after all. Case of the filename didn't match the case I put in the parameter. OSX doesn't care, but the iPad does.
Try removing the slashes in your #define. stringByAppendingPathComponent will add the slashes for you. Not sure why this would work in the Simulator.
Also it is good idea to use:
[NSPropertyListSerialization propertyListWithData:options:format:error:]
Instead of direct NSDictionary initialization. If something will fail, it should return a nice error for you.
+ (id)dictionaryWithContentsOfFile:(NSString *)path
Parameters
path
A full or relative pathname. The file identified by path must contain
a string representation of a property list whose root object is a
dictionary.
Return Value
A new dictionary that contains the dictionary at path, or nil if there
is a file error or if the contents of the file are an invalid
representation of a dictionary.
So, you have to set a full path to your file (or an appropriate relative path)
Note also that the app bundle is read-write on Simulator but read-only on device.
I'm having some trouble debugging my NSLocalizedString implementation. Should be simple, but whatever I do, the function only returns the KEY string.
I'm using XCode 4.5 and iOS6, so I:
Added a new file called File.strings.
In my project settings I added English and Spanish as language settings.
Clicked "Make Localized" in the file inspector, and made sure that both English and Spanish options were selected, and also that the Target membership to my target was selected.
Added "KEY" = "TestEnglish"; to my english File.strings
Added "KEY" = "TestSpanish"; to my spanish File.strings
Added NSLog(#"Localization: %#\n", NSLocalizedString(#"KEY", nil)); to my .m file.
When I run the app, the value "KEY" is always displayed printed in the NSLog.
To jump into this a bit more, I tried this as well:
NSString *path = [[NSBundle mainBundle] pathForResource:#"en" ofType:#"lproj"];
NSString *str = [[NSBundle bundleWithPath:path] localizedStringForKey:#"KEY" value:#"" table:nil];
NSLog(#"Localization: %#\n", str);
and still the value "KEY" is printed, yet, path is a valid path.
Does anyone have any clue how to debug this? I feel like I've read every SO question/answer out there, but none of the suggestions help.
I realize that NSLocalizedString returns the KEY string when it cannot match a key, but I don't see how I can debug why my app might not be matching the KEY.
I've also deleted/cleaned the app about a dozen times.
If you specify table:nil, then NSBundle will try to fetch the localization from the default table (the one in SOMELANG.lproj/Localizable.strings). If you have the localization elsewhere, you should explicitly specify the table using table:#"File" (or use the NSLocalizedStringFromTable() macro in a similar manner:
NSString *value = NSLocalizedStringFromTable(#"key", #"File", nil);
Rename the InfoPlist.strings file to Localizable.strings (double clic) and then you will get the correct string for that key.
In my case the issue was with the case of the string:
"bla.bla.blabla.BookSlot" whereas the Localizable.strings had it defined as "bla.bla.blabla.Bookslot"
So, double-check that the key string is in the correct case. Better yet, copy-paste.
I'm working on a reference app that loads a plist file that can be searched.
The plist file is about 6kb with about 600 entries.
I have been working with the simulator and all works ok, however when I load it onto a device none of the data is there. Just an empty table.
I think the file may be too big because I can load a plist with 20 entries and it loads fine on the device.
If the file was too large wouldn't the whole app just crash?
Does anyone have any suggestions?
This is how I load my plist file
NSString* myFile = [[NSBundle mainBundle] pathForResource:#"myPlistFile" ofType:#"plist"];
array = [[NSMutableArray alloc] initWithContentsOfFile:myFile];
What it sounds like to me is that either your plist isn't even getting copied at all onto the device, or you're using case-sensitive file naming.
First see if the file exists at all:
NSString *myFilePath = [[NSBundle mainBundle] pathForResource:#"myPlistFile" ofType:#"plist"];
BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:myFilePath];
Check that boolean (using the debugger or by logging, either way). If the file exists, then I suspect that you're using a wrong case when trying to access the filename. Filenames are case-sensitive on iOS devices, unlike the Simulator. For example, lets say your plist was named myAwesomeStuff.plist. If you tried to access myawesomestuff.plist on the Simulator, it would work just fine. Not so on the device. Make sure you are using the correct case on your file names.