How do I get values from a tiered .plist? - ios

I've already looked at Parse Plist (NSString) into NSDictionary and deemed it to be not a duplicate, as that question and its answer do not address my concerns.
I have a .plist file in the file system structured like this:
The source code of this .plist file looks like this:
{
"My App" = {
"Side Panel" = {
Items = {
Price = "#123ABC";
};
};
};
}
I know how to get an item in the Root like this:
[[NSBundle mainBundle] pathForResource:#"filename" ofType:#"plist"];
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
NSString value = [dict objectForKey:#"key"]);
But what if the structure is like mine, with tiered dictionaries? How do I get the value of Price?
I would like to do this all in one method, ideally like this:
Calling
NSString *hexString = [self getColorForKey:#"My App.Side Panel.Items.Price"];
Definition
- (NSString *) getColorForKey: (NSString *)key
{
NSArray *path = [key componentsSeparatedByString:#"."];
NSDictionary *colors = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"Colors" ofType:#"plist"]];
NSString *color = #"#FFFFFF"; // white is our backup
// What do I put here to get the color?
return color;
}

Here's the solution that worked for me:
+ (NSString*) getHexColorForKey:(NSString*)key
{
NSArray *path = [key componentsSeparatedByString:#"."];
NSDictionary *colors = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"Colors" ofType:#"plist"]];
NSString *color = #"#FFFFFF";
for (NSString *location in path) {
NSObject *subdict = colors[location];
if ([subdict isKindOfClass:[NSString class]])
{
color = (NSString*)subdict;
break;
}
else if ([subdict isKindOfClass:[NSDictionary class]])
{
colors = (NSDictionary*)subdict; // if it's a dictinoary, our color may be inside it
}
else
{
[SilverLog level:SilverLogLevelError message:#"Unexpected type of dictinoary entry: %#", [subdict class]];
return color;
}
}
return color;
}
where key is an NSString that matches /^[^.]+(\.[^.]+)*$/, meaning it looks like my targeted #"My App.Side Panel.Items.Price".

Yes I understand what you're looking to accomplish; thank you for the clarification. I will however add that the resources and advice I have written do provide the necessary information resolve your problem.
That said, the following gets your dictionary:
NSURL *plistURL = [[NSBundle mainBundle] URLForResource:#"Info" withExtension:#"plist"];
NSData *plistData = [NSData dataWithContentsOfURL:plistURL];
NSDictionary *tieredPlistData = [NSPropertyListSerialization propertyListWithData:plistData
options:kCFPropertyListImmutable
format:NULL
error:nil];
Then, if we're interested in the information contained in Items
NSDictionary *allItemsDictionary = tieredPlistData[#"My App"][#"Side Panel"][#"Items"];
Assuming that Items will contain a number of objects, you could use
NSArray *keys = [allItems allKeys];
for(NSString *key in keys){
NSString *colorValue = allItemsDictionary[key];
// do something with said color value and key
}
Or, if there is a single value you need, then just reference that key
NSString *colorForPriceText = allItemsDictionary[#"Price"];
But a few tips:
It's generally considered a better idea to keep frequently accessed values in code instead of a plist/file that is loaded at runtime.
That said, you wouldn't put your call to load from NSBundle in the same method you would use to query a specific value. In your example, every time you need a color, you end up re-accessing NSBundle and pile on unneeded memory allocations. One method would load the plist into an iVar NSDictionary and then that NSDictionary would be used separately by another method.

Related

Dictionary from plist

i have 3 plist files with named level0, level1, level2, all of this plist have the same structure, it consist of number variable and array. I init my app with data from this plist.
+(instancetype)levelWithNum:(int)levelNum; {
NSString* fileName = [NSString stringWithFormat:#"level%i.plist", levelNum];
NSString* levelPath = [[[NSBundle mainBundle]resourcePath]stringByAppendingPathComponent:fileName];
NSDictionary *levelDic = [NSDictionary dictionaryWithContentsOfFile:levelPath];
NSAssert(levelDic, #"level no loaded");
Level *l = [[Level alloc]init];
l.coinsPerLvl = [levelDic[#"coinsPerLvl"] integerValue];
l.words = levelDic[#"words"];
return l;
}
Now i decide to use only one plist and add in it 3 dictionary. How can i read only one dictionary from plist like in example above where i use plist files.
Tanks for any help!
you only need an intermediate variable thats represents the root dictionary and extract the level dictionary from the root dictionary, e.g:
+(instancetype)levelWithNum:(int)levelNum; {
NSString *levelsPlist = [[NSBundle mainBundle] pathForResource:#"levels" ofType:#"plist"];
NSDictionary *rootDict = [NSDictionary dictionaryWithContentsOfFile: levelsPlist];
NSString *levelKey = [NSString stringWithFormat:#"level%i", levelNum];
NSDictionary *levelDic = rootDict[levelKey];
NSAssert(levelDic, #"level no loaded");
Level *l = [[Level alloc]init];
l.coinsPerLvl = [levelDic[#"coinsPerLvl"] integerValue];
l.words = levelDic[#"words"];
return l;
}
Hope it help.
make your plist root as an array (like below). that way you can get levels info easily.
sample code
+(void)levelWithNum:(int)levelNum {
NSString* fileName = #"level.plist";
NSString* levelPath = [[[NSBundle mainBundle]resourcePath]stringByAppendingPathComponent:fileName];
NSArray *levelsArray = [NSArray arrayWithContentsOfFile:levelPath];
NSAssert(levelsArray, #"level no loaded");
Level *l = [[Level alloc]init];
l.coinsPerLvl = [[levelsArray objectAtIndex:levelNum][#"coinsPerLvl"] integerValue];
l.words = [[levelsArray objectAtIndex:levelNum][#"words"] integerValue];
return l;
}
sample plist screen shot

Lazy load plist dictionary element(s)

The usual method for loading data from a dictionary contained in a plist is as below:
NSString *path = [[NSBundle mainBundle] pathForResource:#"Data" ofType:#"plist"];
NSDictionary *data= [NSDictionary dictionaryWithContentsOfFile:path];
Is there a way to import only the element(s) specified in a key / set of keys, like:
NSDictionary *data= [NSDictionary dictionaryWithContentsOfFile:path forKey:key];
The idea is to perform lazy loading of dictionary contents by key.
So based on my comment above, you could add a class method to the NSDictionary via a category. You could do something like (not tested BTW).
+ (NSDictionary *)dictionaryWithContentsOfFile:(NSString *)path forKeys:(NSArray *)keys
{
NSMutableDictionary *newDictionary = nil;
NSDictionary *dictionary = [NSDictionary dictionaryWithContentsOfFile:path];
if (dictionary) {
newDictionary = [NSMutableDictionary dictionary];
for (id key in dictionary.allKeys) {
if ([keys containsObject:key]) {
newDictionary[key] = dictionary[key];
}
}
}
return [newDictionary copy];
}
If you did this, you'd see your spike in memory, but it should subside once dictionary is freed.
Alternatively, take a look at YAJL (https://github.com/lloyd/yajl). I've used this when dealing with very large JSON files. This was mainly the stream it in chunks. It is event driven, so you should be able to stream it in and detect the keys you want (hopefully).
please try the below method.
- (void)viewDidLoad
{
NSMutableArray *arry;
arry = [[NSMutableArray alloc]initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"Catalog" ofType:#"plist"];
NSDictionary *dic = [menuArray objectAtIndex:indexPath.row];
lblName.text = [dic objectForKey:#"MenuName"];
}

How could I have .plist path inside iPad / iPhone

How could I have a complete list of each .plist path (inside iPhone/iPad) ?
In iOS Simulator I'm using it : ls -l ~/Library/Preferences/com.apple.*.plist
My goal is to find a specific key and read the boolean value in preferences.
Because I need to know if it is enable or not.
It is something that is missing in SDK but is existing in preferences.
NSLog (#"%#", [[NSBundle mainBundle] pathsForResourcesOfType:#"plist" inDirectory:#"/"]);
The method returns an NSArray.
You need to create the plist first. All you have to do is access it when you need it.
NSArray *thePlist;
NSString *plist = [[NSBundle mainBundle] pathForResource: #"plistFileName" ofType: #"plist"];
thePlist = [[NSArray alloc] initWithContentsOfFile: plist];
Thank you Daniel A. White , Jahm and Kedar!
I code this to summarize each comment :
With an extra, add an example to read UISupportedInterfaceOrientations values.
NSLog(#"Preferences plists from apple (inside iPhone/iPad) are not accessible from code");
NSLog (#"Accessible plist paths : %#", [[NSBundle mainBundle] pathsForResourcesOfType:#"plist" inDirectory:#"/"]);
NSArray *pList = [[NSBundle mainBundle] pathsForResourcesOfType:#"plist" inDirectory:#"/"];
for (int i = 0; i < [pList count]; i ++)
{
NSString *plistPath = [pList objectAtIndex:i];
NSDictionary *plistDictionary = [NSDictionary dictionaryWithContentsOfFile:plistPath];
NSMutableArray *nameString = [plistDictionary objectForKey:#"UISupportedInterfaceOrientations"];
for (NSString *n in nameString)
{
NSLog(#"Supported Interface : %#",n);
}
}

iOS release not working as expected

I am using this code to get book names from a config.plist file. However my memory management is problematic. The '[dict release]' breaks the app completely and it exits.
The code works when the '[dict release]' is removed but it causes memory leaks as far as I can tell.
bnames is a global NSMutableArray
What am I doing wrong?
- (NSString *)loadBookname: (NSInteger) bookToLoad {
bookToLoad = [self bookOrder:bookToLoad];
//---get the path to the property list file---
plistFileNameConf = [[self documentsPath] stringByAppendingPathComponent:#"Config.plist"];
//---if the property list file can be found---
if ([[NSFileManager defaultManager] fileExistsAtPath:plistFileNameConf]) {
//---load the content of the property list file into a NSDictionary object---
dict = [[NSDictionary alloc] initWithContentsOfFile:plistFileNameConf];
bnames = [dict valueForKey:#"BookNames"];
[dict release];
}
else {
//---load the property list from the Resources folder---
NSString *pListPath = [[NSBundle mainBundle] pathForResource:#"Config" ofType:#"plist"];
dict = [[NSDictionary alloc] initWithContentsOfFile:pListPath];
bnames = [dict valueForKey:#"BookNames"];
[dict release];
}
plistFileNameConf = nil;
NSString *bookNameTemp;
bookNameTemp = [bnames objectAtIndex:bookToLoad - 1];
NSLog(#"bookName: %#", bookNameTemp);
return bookNameTemp;
}
You need to allocate your array properly:
bnames = [[NSArray alloc] initWithArray:[dict valueForKey:#"BookNames"]];
Double check that your dict returns the right data type.
There does not appear to be anything wrong with the way you allocate NSDictionary (although you could also use the [NSDictionary dictionaryWithContentsOfFile:] and save yourself having to worry about the release.
Either way I would suggest the issue is not with the [release] but probably the line BEFORE release:
bnames = [dict valueForKey:#"BookNames"];
a) Where is that allocated. I don't see an allocation or declaration of it anywhere?
b) What type of value do you expect back?
Put a break point on it and make sure your getting what you expect or anything.
If dict is not already a strong property, make it one. Then, use self.dict when assigning to it (and keep the release).
I've found what appears to be a better solution to the issue. This lets iOS manage the memory.
//---finds the path to the application's Documents directory---
- (NSString *) documentsPath {
NSLog(#"Start documentsPath");
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDir = [paths objectAtIndex:0];
// NSLog(#"Found documentsPath 40");
NSLog(#"End documentsPath");
return documentsDir;
}
- (NSString *) configPath {
NSLog(#"Start configPath");
NSString *plistFileNameConf = [[self documentsPath] stringByAppendingPathComponent:#"Config.plist"];
if (![[NSFileManager defaultManager] fileExistsAtPath:plistFileNameConf]) {
plistFileNameConf = [[NSBundle mainBundle] pathForResource:#"Config" ofType:#"plist"];
}
NSLog(#"plistFile: %#",plistFileNameConf);
NSLog(#"End configPath");
return plistFileNameConf;
}
The following calls the above code as necessary:
NSString *Choice;
NSArray *properties;
NSString *errorDesc = nil;
NSPropertyListFormat format;
NSData *plistXML = [[NSFileManager defaultManager] contentsAtPath:[self configPath]];
NSDictionary *temp = (NSDictionary *)[NSPropertyListSerialization propertyListFromData:plistXML mutabilityOption:NSPropertyListMutableContainersAndLeaves format:&format errorDescription:&errorDesc];
if (!temp) {
NSLog(#"Error reading plist: %#, format: %d", errorDesc, format);
}
Choice = [temp objectForKey:#"Choice"];
properties = [temp objectForKey:Choice];

Plist to NSDictionary

I have some plist data like the one on the below image:
How do I access the "Text" value, if an NSDate equal to CDate is selected on my UIDatePicker?
So you basically want
NSString* path = [[NSBundle mainBundle] pathForResource:#"file" ofType:#"plist"];
NSArray* a = [NSArray arrayWithContentsOfFile:path];
for (NSDictionary *d in a)
{
NSString *cdate = [d objectForKey:#"CDate"];
if ([cdate isEqualToString:#"Whatever is currently selected"])
{
NSString *text = [d objectForKey:#"Text"];
// do something with text here
}
}
Stumbled across this and thought an update was due
NSDictionary *initialDefaults = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"Defaults" ofType:#"plist"]];

Resources