Objective-c: NSMutableDictionary setObject not working - objective-c

Not sure what I am doing wrong here. When I try to check the dictionary either by a specific key or allkeys I either get an error or null. (I know I'm using a string where I could be using a boolean for the conditional I just like having a check like that say true or false instead of YES and NO. Add that to my OCD list. :D ) activePlayer is set in an awakeFromNib method to 1, it can be switched using a popupbutton between P1 and P2.
- (IBAction)setPlayer:(id)sender {
haserror = #"false";
errmsg = [NSMutableString stringWithCapacity:0];
[errmsg retain];
[errmsg appendString: #"There was a problem setting your team up\n\n"];
thisTeamName = [txtTeamName stringValue];
thisTeamColor = [pdTeamColor itemTitleAtIndex:[pdTeamColor indexOfSelectedItem]];
//validate form
if ([thisTeamName isEqualToString:#""]) {
haserror = #"true";
[errmsg appendString: #"You must enter a team name\n\n"];
}
if ([thisTeamColor isEqualToString:#"Select A Color"]) {
haserror = #"true";
[errmsg appendString: #"You must select a team color\n\n"];
}
//check for errors
if (haserror == #"true") {
[self showAlert: errmsg];
} else {
//set up treasury
treasury = 1000;
//convert to string for display
[lblTreasury setStringValue: [NSString stringWithFormat:#"$%i", treasury] ];
//add items to dictionary
if (activePlayer == #"1") {
[p1TeamData setObject:thisTeamName forKey:#"teamName"];
[p1TeamData setObject:thisTeamColor forKey:#"teamColor"];
[p1TeamData setObject:[NSString stringWithFormat:#"%i", treasury] forKey:#"cash"];
} else {
[p2TeamData setObject:thisTeamName forKey:#"teamName"];
[p2TeamData setObject:thisTeamColor forKey:#"teamColor"];
[p2TeamData setObject:[NSString stringWithFormat:#"%i", treasury] forKey:#"cash"];
}
NSLog(#"%#", [p1TeamData allKeys]);
}
[errmsg release];
}
[Edit: here's the .h file]
#interface GameController :NSObject {
IBOutlet id btnSaveData;
IBOutlet id lblTreasury;
IBOutlet id pdPickPlayer;
IBOutlet id pdTeamColor;
IBOutlet id txtTeamName;
int activePlayer;
NSString* activePlayerName;
NSString* activePlayerTeamColor;
int treasury;
NSMutableDictionary* p1TeamData;
NSMutableDictionary* p2TeamData;
NSArray* players;
NSArray* teamColors;
NSArray* unittypes;
NSString* thisTeamName;
NSString* thisTeamColor;
NSMutableString* errmsg;
NSString* haserror;
}
-(void) awakeFromNib;
- (IBAction) getPlayer : (id)sender;
- (IBAction) setPlayer : (id)sender;
-(void) showAlert : (NSMutableString* ) m;
#end

Make sure you initialize the collections in the -initXXX method. If not, they will be assigned to nil.
-(id)initXXX:... {
if ((self = [super initYYY:...])) {
...
p1TeamData = [[NSMutableDictionary alloc] init];
p2TeamData = [[NSMutableDictionary alloc] init];
...
}
return self;
}
If all you want are "true" and "false", just define them yourself. It's not a reason to use string instead of BOOL. In fact, Foundation already defined TRUE and FALSE besides YES and NO.
Also, please use an integer for activePlayer.
You should always compare NSString with -isEqualToString:, not ==.
if ([haserror isEqualToString:#"true"])
...
if ([activePlayer isEqualToString:#"1"])
This should be the reason why p1TeamData is always nil, because activePlayer == #"1" is unreliable and there could be player-1 stuff assigned to p2TeamData.

Related

Archiving objects within objects in objective-c

Thankyou for reading,
PS: I am a beginner so I am not too good at this unfortunetaly, but any help would be very appreciated
So basically I want to archive a big array which contains Account objects, which they themselves contain:
1.a username in form of a NSString,
2.an encrypted password array filled with NSNumbers, and
3.a data array filled with service data objects.
The service data objects have the following:
encrypted serviceType (NSArray filled with NSNumbers) (whatever service the username and password is for)
encrypted username (NSArray filled with NSNumbers)
encrypted password (NSArray filled with NSNumbers)
Now weirdly when trying to archive and save this, I get two errors. One time it won't let me add service data objects to the data array in the Account class anymore, with the following error message (or at least they dont show up in the NSTableView I have, however it does say they exsist):
[<ServiceData 0x60000023bfa0> valueForUndefinedKey:]: this class is not key value
coding-compliant for the key service.
and two, when I try to login in the the username and password from the Account class, it retrieves the username and the first couple and last couple NSNumbers of my password correctly, but the rest of the NSNumbers for the password are in the trillions or something, so I'm wondering what is going wrong, any help would be greatly appreciated.
Here is the code for my instance variables, how I used the NSKeyedArchiver and unarchiver, and how I went about saving and loading the files. Again, please help me, I have been stuck on this for a while and this is kind-of my last resort. I have no idea what is happening!
ACCOUNT CLASS:
H file:
#interface Account : NSObject <NSCoding>
{
#private
NSString *username;
NSMutableArray *password;
NSMutableArray *accData;
}
#property NSString *username;
#property NSArray *password;
FULL M file:
-(id)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if(self)
{
username = [aDecoder decodeObjectForKey:#"username"];
password = [aDecoder decodeObjectForKey:#"password"];
accData = [aDecoder decodeObjectForKey:#"data"];
}
return self;
}
-(void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:username forKey:#"username"];
[aCoder encodeObject:password forKey:#"password"];
[aCoder encodeObject:accData forKey:#"data"];
}
SERVICEDATA CLASS:
H file:
#interface ServiceData : NSObject <NSCoding>
{
#private
NSArray* serviceData;
NSArray* usernameData;
NSArray* passwordData;
}
#property NSArray* serviceData;
#property NSArray* usernameData;
#property NSArray* passwordData;
M file:
#import "Account.h"
#import "Crypt.h"
NSMutableArray *accounts;
NSInteger accountNumber = -1;
#implementation Account
#synthesize username;
#synthesize password;
- (id)initWithUsername:(NSString *)name withPassword:(NSMutableArray *)key
{
self = [super init];
if (self)
{
username = name;
password = key;
accData = [[NSMutableArray alloc]init];
}
return self;
}
/*
setters and getters
*/
-(NSString*)getUsername;
{
return username;
}
-(NSArray*)getPassword;
{
return password;
}
-(void)changePassword:(NSMutableArray*)newPassword;
{
NSInteger sizeOldPass = [password count];
NSInteger sizeNewPass = [newPassword count];
int changeXObjects = (int)(sizeNewPass - sizeOldPass);
int changeSize = abs(changeXObjects);
//adjusts size differences
if (changeXObjects < 0)
{
for(int i = 0; i < changeSize; i++)
{
[password removeLastObject];
}
}
else if (changeXObjects > 0)
{
for(int i = 0; i < changeSize; i++)
{
NSNumber *value = [NSNumber numberWithInt:0];
[password addObject:value];
}
}
//change password
NSInteger sizePass = [password count];
for (int k = 0; k < sizePass; k++)
{
[password replaceObjectAtIndex:k withObject:newPassword[k]];
}
}
-(NSMutableArray*)getAccData;
{
return accData;
}
-(void)setAccData:(NSMutableArray*)input
{
[input setArray: accData];
}
+(NSMutableArray*)getAccounts
{
return accounts;
}
+(NSInteger)getAccountNumber
{
return accountNumber;
}
+(void)setAccounts:(id)accs
{
accounts = accs;
}
+(void)setAccountNumber:(NSInteger)number
{
accountNumber = number;
}
/*
other methods
*/
+(void)addAccount:(id)acc
{
[accounts addObject:acc];
}
+(void)deleteAccount:(NSInteger)index
{
[accounts removeObjectAtIndex:index];
}
-(void)addAccData:(id)input
{
[accData addObject:input];
}
-(void)deleteAccDataAt:(NSInteger)index
{
[accData removeObjectAtIndex:index];
}
+(bool)checkPassword:(NSString*)passwordIn accountNumber:(NSInteger)index
{
NSMutableArray *passwordInputCrypt = [Crypt encrypt:passwordIn];
NSMutableArray *passwordCrypt = [accounts[index] getPassword];
NSInteger lengthPassword = [passwordInputCrypt count];
bool correctPassword = true;
if([passwordCrypt count] == [passwordInputCrypt count])
{
for(int i = 0; i < lengthPassword; i++)
{
if(passwordCrypt[i]!=passwordInputCrypt[i])
correctPassword = false;
}
}
else
{
correctPassword = false;
}
if(correctPassword == true)
{
return true;
}
return false;
}
-(id)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if(self)
{
username = [aDecoder decodeObjectForKey:#"username"];
password = [aDecoder decodeObjectForKey:#"password"];
accData = [aDecoder decodeObjectForKey:#"data"];
}
return self;
}
-(void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:username forKey:#"username"];
[aCoder encodeObject:password forKey:#"password"];
[aCoder encodeObject:accData forKey:#"data"];
}
#end
LOADING FILE(filePath is given):
NSData *data = [[NSFileManager defaultManager] contentsAtPath:filePath];
if(data != nil)
{
NSArray *arrayFromData = [NSKeyedUnarchiver unarchiveObjectWithData:data];
NSMutableArray *initArray = [NSMutableArray arrayWithArray:arrayFromData];
[Account setAccounts:initArray];
}
else
{
NSMutableArray *accountsInit = [[NSMutableArray alloc] init];
[Account setAccounts:accountsInit];
}
SAVING FILE:
NSArray *accounts = [Account getAccounts];
NSString *filePath = [AppController getFilePath];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:accounts];
[data writeToFile:filePath atomically:YES];
A few things:
You should not be archiving password data to disk (even if you are encrypting it). That's what the keychain is for. Have a look at SSKeychain for a good wrapper class.
The Key-value coding error you are getting suggests you are trying to reference your serviceData array as just "service" somewhere. Check your valueForKey and setValueForKey statements.
Can you post the rest of the Account class? That method setAccounts looks like it might be relevant.
Also is there a reason you are using Keyed Archiving instead of Core Data?

Converting NSObject to NSDictionary

Hello I a class of type NSObject:
ProductDetails *details = [[ProductDetails alloc] init];
details.name = #"Soap1";
details.color = #"Red";
details.quantity = 4;
I want to pass the "details" object to a dictionary.
I did,
NSDictionary *dict = [NSDictionary dictionaryWithObject:details forKey:#"details"];
I am passing this dict to another method which performs a check on JSONSerialization:
if(![NSJSONSerialization isValidJSONObject:dict])
And I am getting a crash on this check. Am I doing anything wrong here? I know that the details I am getting is a JSON object and I am assigning it to the properties in my ProductDetails class.
Please help me. I am a noob in Objective-C.
I now tried:
NSError* error;
NSDictionary* json = [NSJSONSerialization JSONObjectWithData:(NSData*)details options:kNilOptions error:&error];
All I need here is an easy way to convert details to NSData.
I noticed that I have an array inside my object may be thats why all the ways I tried is throwing an exception. However since this question is becoming to big, I have started an another question thread for it where I have displayed the data I am getting inside the object - https://stackoverflow.com/questions/19081104/convert-nsobject-to-nsdictionary
This may well be the easiest way to achieve it. Do import #import <objc/runtime.h> in your class file.
#import <objc/runtime.h>
ProductDetails *details = [[ProductDetails alloc] init];
details.name = #"Soap1";
details.color = #"Red";
details.quantity = 4;
NSDictionary *dict = [self dictionaryWithPropertiesOfObject: details];
NSLog(#"%#", dict);
//Add this utility method in your class.
- (NSDictionary *) dictionaryWithPropertiesOfObject:(id)obj
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
unsigned count;
objc_property_t *properties = class_copyPropertyList([obj class], &count);
for (int i = 0; i < count; i++) {
NSString *key = [NSString stringWithUTF8String:property_getName(properties[i])];
[dict setObject:[obj valueForKey:key] forKey:key];
}
free(properties);
return [NSDictionary dictionaryWithDictionary:dict];
}
NSDictionary *details = {#"name":product.name,#"color":product.color,#"quantity":#(product.quantity)};
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:details
options:NSJSONWritingPrettyPrinted // Pass 0 if you don't care about the readability of the generated string
error:&error];
if (! jsonData) {
NSLog(#"Got an error: %#", error);
} else {
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}
Second part's source: Generate JSON string from NSDictionary in iOS
As mmackh said, you want to define a custom method for your ProductDetails object that will return a simple NSDictionary of values, e.g.:
#implementation ProductDetails
- (id)jsonObject
{
return #{#"name" : self.name,
#"color" : self.color,
#"quantity" : #(self.quantity)};
}
...
Let's assume that we added manufacturer property to our ProductDetails, which referenced a ManufacturerDetails class. We'd just write a jsonObject for that class, too:
#implementation ManufacturerDetails
- (id)jsonObject
{
return #{#"name" : self.name,
#"address1" : self.address1,
#"address2" : self.address2,
#"city" : self.city,
...
#"phone" : self.phone};
}
...
And then change the jsonObject for ProductDetails to employ that, e.g.:
#implementation ProductDetails
- (id)jsonObject
{
return #{#"name" : self.name,
#"color" : self.color,
#"quantity" : #(self.quantity),
#"manufacturer" : [self.manufacturer jsonObject]};
}
...
If you have potentially nested collection objects (arrays and/or dictionaries) with custom objects that you want to encode, you could write a jsonObject method for each of those, too:
#interface NSDictionary (JsonObject)
- (id)jsonObject;
#end
#implementation NSDictionary (JsonObject)
- (id)jsonObject
{
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if ([obj respondsToSelector:#selector(jsonObject)])
[dictionary setObject:[obj jsonObject] forKey:key];
else
[dictionary setObject:obj forKey:key];
}];
return [NSDictionary dictionaryWithDictionary:dictionary];
}
#end
#interface NSArray (JsonObject)
- (id)jsonObject;
#end
#implementation NSArray (JsonObject)
- (id)jsonObject
{
NSMutableArray *array = [NSMutableArray array];
[self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if ([obj respondsToSelector:#selector(jsonObject)])
[array addObject:[obj jsonObject]];
else
[array addObject:obj];
}];
return [NSArray arrayWithArray:array];
}
#end
If you do something like that, you can now convert arrays or dictionaries of your custom objects object into something that can be used for generating JSON:
NSArray *products = #[[[Product alloc] initWithName:#"Prius" color:#"Green" quantity:3],
[[Product alloc] initWithName:#"Accord" color:#"Black" quantity:1],
[[Product alloc] initWithName:#"Civic" color:#"Blue" quantity:2]];
id productsJsonObject = [products jsonObject];
NSError *error = nil;
NSData *data = [NSJSONSerialization dataWithJSONObject:productsJsonObject options:0 error:&error];
If you're simply trying to save these objects in a file, I'd suggest NSKeyedArchiver and NSKeyedUnarchiver. But if you need to generate JSON objects for your own private classes, you can do something like the above might work.
In .h File
#import <Foundation/Foundation.h>
#interface ContactDetail : NSObject
#property (nonatomic) NSString *firstName;
#property (nonatomic) NSString *lastName;
#property (nonatomic) NSString *fullName;
#property (nonatomic) NSMutableArray *mobileNumbers;
#property (nonatomic) NSMutableArray *Emails;
#property (assign) bool Isopen;
#property (assign) bool IsChecked;
-(NSDictionary *)dictionary;
#end
in .m file
#import "ContactDetail.h"
#import <objc/runtime.h>
#implementation ContactDetail
#synthesize firstName;
#synthesize lastName;
#synthesize fullName;
#synthesize mobileNumbers;
#synthesize Emails;
#synthesize IsChecked,Isopen;
//-(NSDictionary *)dictionary {
// return [NSDictionary dictionaryWithObjectsAndKeys:self.fullName,#"fullname",self.mobileNumbers,#"mobileNumbers",self.Emails,#"emails", nil];
//}
- (NSDictionary *)dictionary {
unsigned int count = 0;
NSMutableDictionary *dictionary = [NSMutableDictionary new];
objc_property_t *properties = class_copyPropertyList([self class], &count);
for (int i = 0; i < count; i++) {
NSString *key = [NSString stringWithUTF8String:property_getName(properties[i])];
id value = [self valueForKey:key];
if (value == nil) {
// nothing todo
}
else if ([value isKindOfClass:[NSNumber class]]
|| [value isKindOfClass:[NSString class]]
|| [value isKindOfClass:[NSDictionary class]] || [value isKindOfClass:[NSMutableArray class]]) {
// TODO: extend to other types
[dictionary setObject:value forKey:key];
}
else if ([value isKindOfClass:[NSObject class]]) {
[dictionary setObject:[value dictionary] forKey:key];
}
else {
NSLog(#"Invalid type for %# (%#)", NSStringFromClass([self class]), key);
}
}
free(properties);
return dictionary;
}
#end
if any crash ,You check the property (NSMutableArray,NSString,etc ) in else if condition inside of for.
In Your Controller, in any func...
-(void)addItemViewController:(ConatctViewController *)controller didFinishEnteringItem:(NSMutableArray *)SelectedContact
{
NSLog(#"%#",SelectedContact);
NSMutableArray *myData = [[NSMutableArray alloc] init];
for (ContactDetail *cont in SelectedContact) {
[myData addObject:[cont dictionary]];
}
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:myData options:NSJSONWritingPrettyPrinted error:&error];
if ([jsonData length] > 0 &&
error == nil){
// NSLog(#"Successfully serialized the dictionary into data = %#", jsonData);
NSString *jsonString = [[NSString alloc] initWithData:jsonData
encoding:NSUTF8StringEncoding];
NSLog(#"JSON String = %#", jsonString);
}
else if ([jsonData length] == 0 &&
error == nil){
NSLog(#"No data was returned after serialization.");
}
else if (error != nil){
NSLog(#"An error happened = %#", error);
}
}
Try this:
#import <objc/runtime.h>
+ (NSDictionary *)dictionaryWithPropertiesOfObject:(id)obj {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
unsigned count;
objc_property_t *properties = class_copyPropertyList([obj class], &count);
for (int i = 0; i < count; i++) {
NSString *key = [NSString stringWithUTF8String:property_getName(properties[i])];
[dict setObject:[obj valueForKey:key] ? [obj valueForKey:key] : #"" forKey:key];
}
free(properties);
return [NSDictionary dictionaryWithDictionary:dict];
}
The perfect way to do this is by using a library for serialization/deserialization
many libraries are available but one i like is
JagPropertyConverter
https://github.com/jagill/JAGPropertyConverter
it can convert your Custom object into NSDictionary and vice versa
even it support to convert dictionary or array or any custom object within your object (i.e Composition)
JAGPropertyConverter *converter = [[JAGPropertyConverter alloc]init];
converter.classesToConvert = [NSSet setWithObjects:[ProductDetails class], nil];
//For Object to Dictionary
NSDictionary *dictDetail = [converter convertToDictionary:detail];
NSDictionary* json = [NSJSONSerialization JSONObjectWithData:dictDetail options:NSJSONWritingPrettyPrinted error:&error];
You can convert object (say modelObject) to dictionary at runtime with the help of objc/runtime.h class but that has certain limitations and is not recommended.
Considering MVC, mapping logic should be implemented in Model class.
#interface ModelObject : NSObject
#property (nonatomic) NSString *p1;
#property (nonatomic) NSString *p2;
-(NSDictionary *)dictionary;
#end
#import "ModelObject.h"
#implementation ModelObject
-(NSDictionary *)dictionary
{
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
[dict setValue:self.p1 forKey:#"p1"];// you can give different key name here if you want
[dict setValue:self.p2 forKey:#"p2" ];
return dict;
}
#end
Uses:
NSDictionary *modelObjDict = [modelObj dictionary];
Try using
NSDictionary *dict = [details valuesForAttributes:#[#"name", #"color"]];
And compare what the dictionary contains. Then try to convert it to JSON. And look at the JSON spec - what data types can go into a JSON encoded file?
You also can use the NSObject+APObjectMapping category which is available on GitHub: https://github.com/aperechnev/APObjectMapping
It's a quit easy. Just describe the mapping rules in your class:
#import <Foundation/Foundation.h>
#import "NSObject+APObjectMapping.h"
#interface MyCustomClass : NSObject
#property (nonatomic, strong) NSNumber * someNumber;
#property (nonatomic, strong) NSString * someString;
#end
#implementation MyCustomClass
+ (NSMutableDictionary *)objectMapping {
NSMutableDictionary * mapping = [super objectMapping];
if (mapping) {
NSDictionary * objectMapping = #{ #"someNumber": #"some_number",
#"someString": #"some_string" };
}
return mapping
}
#end
And then you can easily map your object to dictionary:
MyCustomClass * myObj = [[MyCustomClass alloc] init];
myObj.someNumber = #1;
myObj.someString = #"some string";
NSDictionary * myDict = [myObj mapToDictionary];
Also you can parse your object from dictionary:
NSDictionary * myDict = #{ #"some_number": #123,
#"some_string": #"some string" };
MyCustomClass * myObj = [[MyCustomClass alloc] initWithDictionary:myDict];
Swift
Now the swift is very popular and most of the SDK's are written in Objective C, we need to convert NSObject to NSDictionary, With the Help of #thatzprem Answer, I wrote an extension for Swift which will convert our NSObject into NSDictionary, then we can use that NSDictionary to simple Dictionary or JSON Object or other purpose. I hope so this will help out the Swift User.
extension NSObject {
func convertNSObjectToNSDictionary() -> [AnyHashable : Any]? {
var dict: [AnyHashable : Any] = [:]
var count: UInt32 = 0
let properties = class_copyPropertyList(type(of: self), UnsafeMutablePointer<UInt32>(mutating: &count)) //as? objc_property_t
for i in 0..<Int(count) {
var key: String? = nil
if let property = properties?[i] as? objc_property_t {
key = String(utf8String: property_getName(property))
}
//dict[key] = (obj as? NSObject)?.value(forKey: key ?? "")
dict[key] = (self).value(forKey: key ?? "")
}
free(properties)
return dict
}
}

Obj-C easy method to convert from NSObject with properties to NSDictionary?

I ran across something that I eventually figured out, but think that there's probably a much more efficient way to accomplish it.
I had an object (an NSObject which adopted the MKAnnotation protocol) that had a number of properties (title, subtitle,latitude,longitude, info, etc.). I needed to be able to pass this object to another object, which wanted to extract info from it using objectForKey methods, as an NSDictionary (because that's what it was getting from another view controller).
What I ended up doing was create a new NSMutableDictionary and use setObject: forKey on it to transfer each piece of vital info, and then I just passed on the newly created dictionary.
Was there an easier way to do this?
Here's the relevant code:
// sender contains a custom map annotation that has extra properties...
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"showDetailFromMap"])
{
DetailViewController *dest =[segue destinationViewController];
//make a dictionary from annotaion to pass info
NSMutableDictionary *myValues =[[NSMutableDictionary alloc] init];
//fill with the relevant info
[myValues setObject:[sender title] forKey:#"title"] ;
[myValues setObject:[sender subtitle] forKey:#"subtitle"];
[myValues setObject:[sender info] forKey:#"info"];
[myValues setObject:[sender pic] forKey:#"pic"];
[myValues setObject:[sender latitude] forKey:#"latitude"];
[myValues setObject:[sender longitude] forKey:#"longitude"];
//pass values
dest.curLoc = myValues;
}
}
Thanks in advance for your collective wisdom.
Here's what I came up with, thanks to the folks, below...
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"showDetailFromMap"])
{
DetailViewController *dest =[segue destinationViewController];
NSArray *myKeys = [NSArray arrayWithObjects:
#"title",#"subtitle",#"info",#"pic",#"latitude",#"longitude", nil];
//make a dictionary from annotaion to pass info
NSDictionary *myValues =[sender dictionaryWithValuesForKeys:myKeys];
//pass values
dest.curLoc = myValues;
}
}
And a even simpler fix, as seen below...
Using valueForKey instead of object for key to retrieve the information.
Sure thing! Use the objc-runtime and KVC!
#import <objc/runtime.h>
#interface NSDictionary(dictionaryWithObject)
+(NSDictionary *) dictionaryWithPropertiesOfObject:(id) obj;
#end
#implementation NSDictionary(dictionaryWithObject)
+(NSDictionary *) dictionaryWithPropertiesOfObject:(id)obj
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
unsigned count;
objc_property_t *properties = class_copyPropertyList([obj class], &count);
for (int i = 0; i < count; i++) {
NSString *key = [NSString stringWithUTF8String:property_getName(properties[i])];
[dict setObject:[obj valueForKey:key] forKey:key];
}
free(properties);
return [NSDictionary dictionaryWithDictionary:dict];
}
#end
And you would use like this:
MyObj *obj = [MyObj new];
NSDictionary *dict = [NSDictionary dictionaryWithPropertiesOfObject:obj];
NSLog(#"%#", dict);
This is an old post and Richard J. Ross III's answer is really helpful, but in case of custom objects (an custom class has another custom object in it). However, sometimes properties are other objects and so forth, making the serialization a bit complicated.
Details * details = [[Details alloc] init];
details.tomato = #"Tomato 1";
details.potato = #"Potato 1";
details.mangoCount = [NSNumber numberWithInt:12];
Person * person = [[Person alloc]init];
person.name = #"HS";
person.age = #"126 Years";
person.gender = #"?";
person.details = details;
For converting these type of objects (multiple custom objects) into dictionary, I had to modify Richard J. Ross III's Answer a little bit.
+(NSDictionary *) dictionaryWithPropertiesOfObject:(id)obj
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
unsigned count;
objc_property_t *properties = class_copyPropertyList([obj class], &count);
for (int i = 0; i < count; i++) {
NSString *key = [NSString stringWithUTF8String:property_getName(properties[i])];
Class classObject = NSClassFromString([key capitalizedString]);
if (classObject) {
id subObj = [self dictionaryWithPropertiesOfObject:[obj valueForKey:key]];
[dict setObject:subObj forKey:key];
}
else
{
id value = [obj valueForKey:key];
if(value) [dict setObject:value forKey:key];
}
}
free(properties);
return [NSDictionary dictionaryWithDictionary:dict];
}
I hope it will help someone. Full credit goes to Richard J. Ross III.
If the properties had the same names as the keys used to access the dictionary then you could have just used KVC and had valueForKey: instead of objectForKey.
For example given this dictionary
NSDictionary *annotation = [[NSDictionary alloc] initWithObjectsAndKeys:
#"A title", #"title", nil];
and this Object
#interface MyAnnotation : NSObject
#property (nonatomic, copy) NSString *title;
#end
it wouldn't matter if I had an instance of the dictionary or MyAnnotation I could call
[annotation valueForKey:#"title"];
Obviously that works the other way as well e.g.
[annotation setValue:#"A title" forKey:#"title"];
To complete the method of Richard J. Ross, this one works with NSArray of custom object.
+(NSDictionary *) dictionaryWithPropertiesOfObject:(id)obj
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
unsigned count;
objc_property_t *properties = class_copyPropertyList([obj class], &count);
for (int i = 0; i < count; i++) {
NSString *key = [NSString stringWithUTF8String:property_getName(properties[i])];
Class classObject = NSClassFromString([key capitalizedString]);
id object = [obj valueForKey:key];
if (classObject) {
id subObj = [self dictionaryWithPropertiesOfObject:object];
[dict setObject:subObj forKey:key];
}
else if([object isKindOfClass:[NSArray class]])
{
NSMutableArray *subObj = [NSMutableArray array];
for (id o in object) {
[subObj addObject:[self dictionaryWithPropertiesOfObject:o] ];
}
[dict setObject:subObj forKey:key];
}
else
{
if(object) [dict setObject:object forKey:key];
}
}
free(properties);
return [NSDictionary dictionaryWithDictionary:dict];
}
There are so many solutions and nothing worked for me as I had a complex nested object structure. This solution takes things from Richard and Damien but improvises as Damien's solution is tied to naming keys as class names.
Here is the header
#interface NSDictionary (PropertiesOfObject)
+(NSDictionary *) dictionaryWithPropertiesOfObject:(id)obj;
#end
Here is the .m file
#implementation NSDictionary (PropertiesOfObject)
static NSDateFormatter *reverseFormatter;
+ (NSDateFormatter *)getReverseDateFormatter {
if (!reverseFormatter) {
NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:#"en_US_POSIX"];
reverseFormatter = [[NSDateFormatter alloc] init];
[reverseFormatter setDateFormat:#"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"];
[reverseFormatter setLocale:locale];
}
return reverseFormatter;
}
+ (NSDictionary *)dictionaryWithPropertiesOfObject:(id)obj {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
unsigned count;
objc_property_t *properties = class_copyPropertyList([obj class], &count);
for (int i = 0; i < count; i++) {
NSString *key = [NSString stringWithUTF8String:property_getName(properties[i])];
id object = [obj valueForKey:key];
if (object) {
if ([object isKindOfClass:[NSArray class]]) {
NSMutableArray *subObj = [NSMutableArray array];
for (id o in object) {
[subObj addObject:[self dictionaryWithPropertiesOfObject:o]];
}
dict[key] = subObj;
}
else if ([object isKindOfClass:[NSString class]]) {
dict[key] = object;
} else if ([object isKindOfClass:[NSDate class]]) {
dict[key] = [[NSDictionary getReverseDateFormatter] stringFromDate:(NSDate *) object];
} else if ([object isKindOfClass:[NSNumber class]]) {
dict[key] = object;
} else if ([[object class] isSubclassOfClass:[NSObject class]]) {
dict[key] = [self dictionaryWithPropertiesOfObject:object];
}
}
}
return dict;
}
#end
You also can use the NSObject+APObjectMapping category which is available on GitHub: https://github.com/aperechnev/APObjectMapping
It's a quit easy. Just describe the mapping rules in your class:
#import <Foundation/Foundation.h>
#import "NSObject+APObjectMapping.h"
#interface MyCustomClass : NSObject
#property (nonatomic, strong) NSNumber * someNumber;
#property (nonatomic, strong) NSString * someString;
#end
#implementation MyCustomClass
+ (NSMutableDictionary *)objectMapping {
NSMutableDictionary * mapping = [super objectMapping];
if (mapping) {
NSDictionary * objectMapping = #{ #"someNumber": #"some_number",
#"someString": #"some_string" };
}
return mapping
}
#end
And then you can easily map your object to dictionary:
MyCustomClass * myObj = [[MyCustomClass alloc] init];
myObj.someNumber = #1;
myObj.someString = #"some string";
NSDictionary * myDict = [myObj mapToDictionary];
Also you can parse your object from dictionary:
NSDictionary * myDict = #{ #"some_number": #123,
#"some_string": #"some string" };
MyCustomClass * myObj = [[MyCustomClass alloc] initWithDictionary:myDict];

Objective-C for Dummies: How do I loop through an NSDictionary inside of an NSDictionary?

Alright guys, I'm quite confused. So, I have an NSDictionary which is populated by a JSON string which looks like:
{"Success":true,"Devices":[{"UDId":"...","User":"...","Latitude":0.0,"Longitude":0.0}]}
Now, I know how to check if Success is true, but I need to loop through the array of Devices (JSON object) and create an internal array of Devices (internal app object) and I have no idea how to do that. Can someone please explain how to do it?
Here's my Device.m/h:
#import <CoreLocation/CoreLocation.h>
#import <Foundation/Foundation.h>
#interface Device : NSObject {
NSString *udId;
NSString *name;
NSNumber *latitude;
NSNumber *longitude;
}
#property (nonatomic, retain) NSString *udId;
#property (nonatomic, retain) NSString *name;
#property (nonatomic, retain) NSNumber *latitude;
#property (nonatomic, retain) NSNumber *longitude;
#pragma mark -
#pragma mark MKAnnotation Properties
#property (nonatomic, readonly) CLLocationCoordinate2D coordinate;
#end
----
#import "Device.h"
#implementation Device
#synthesize udId, name, latitude, longitude;
- (CLLocationCoordinate2D)coordinate {
CLLocationCoordinate2D internalCoordinate;
internalCoordinate.latitude = [self.latitude doubleValue];
internalCoordinate.longitude = [self.longitude doubleValue];
return internalCoordinate;
}
- (void)dealloc {
[udId release];
udId = nil;
[name release];
name = nil;
[latitude release];
latitude = nil;
[longitude release];
longitude = nil;
[super dealloc];
}
#end
And here's the methods where I should be reading the response and converting it to objects I can use:
- (void)requestFinished:(ASIHTTPRequest *)request {
if (![request error]) {
NSError *jsonError = nil;
NSDictionary *jsonDictionary = [NSDictionary dictionaryWithJSONString:[request responseString] error:&jsonError];
if (!jsonError || ([[jsonDictionary objectForKey:#"Success"] intValue] == 1)) {
// READ "DEVICES" AND CONVERT TO OBJECTS
} else {
// AUTHORIZATION FAILED
}
}
}
I'd really appreciate some help on this. I just can't seem to wrap my head around it...
Thanks in advance!
You are almost there. In your code where you say:
// READ "DEVICES" AND CONVERT TO OBJECTS
do this:
NSArray * devices = [jsonDictionary objectForKey:#"Devices"];
for(NSDictionary * deviceInfo in devices) {
Device * d = [[[Device alloc] init] autorelease];
[d setLatitude:[deviceInfo objectForKey:#"Latitude"]];
[d setLongitude:[deviceInfo objectForKey:#"Longitude"]];
[d setName:[deviceInfo objectForKey:#"User"]];
[d setUdId:[deviceInfo objectForKey:#"UDId"]];
// do some stuff with d
}
What's going on here: I didn't see what JSON library you are using to convert, but presuming it works like TouchJSON or SBJSON, the JSON array is automatically turned into an NSArray instance, while the inner hashes of the NSArray are NSDictionary objects. At the point that you have deserialized that JSON string, everything you're dealing with will be instances of NSString, NSNumber, NSArray and NSDictionary (and depending on the library, NSNull to represent null values).
First you need to define your initializer/constructor for your Device class.
Device.h
- (id)initWithUdid:(NSString *)udid name:(NSString *)name latitude:(NSNumber *)lat longitude:(NSNumber *)lon;
Device.m
- (id)initWithUdid:(NSString *)udid name:(NSString *)name latitude:(NSNumber *)lat longitude:(NSNumber *)lon {
if (self = [super init]) {
self.udid = udid;
self.name = name;
self.latitude = lat;
self.longitude = lon;
}
return self;
}
Then you can initialize a new object like:
Device *dev = [[Device alloc] initWithUdid:#"a udid" name:#"the name" latitude:latNum longitude:lonNum];
So, you should be able to iterate the array and build your Device objects like so:
NSArray *devicesArray = [dict objectForKey:#"Devices"];
for (NSDictionary *d in devicesArray) {
Device *dev = [[Device alloc] initWithUdid:[d objectForKey:#"UDId"]
name:[d objectForKey:#"User"]
latitude:[NSNumber numberWithDouble:[d objectForKey:#"Latitude"]]
longitude:[NSNumber numberWithDouble:[d objectForKey:#"Latitude"]]];
}
You want to access the array of device dictionaries from the top-level dictionary just as you did the Success value. Then iterating over the dictionaries you can use each's -keyEnumerator method to iterate over its keys.
- (void)requestFinished:(ASIHTTPRequest *)request {
if (![request error]) {
NSError *jsonError = nil;
NSDictionary *jsonDictionary = [NSDictionary dictionaryWithJSONString:[request responseString] error:&jsonError];
if (!jsonError || ([[jsonDictionary objectForKey:#"Success"] intValue] == 1)) {
NSArray* deviceArray = [jsonDictionary objectForKey:#"Devices"];
for(NSDictionary* dict in deviceArray)
{
for(NSString* key in [dict keyEnumerator])
{
NSLog(#"%# -> %#", key, [dict objectForKey:key]);
}
}
// READ "DEVICES" AND CONVERT TO OBJECTS
} else {
// AUTHORIZATION FAILED
}
}
}
Sounds like you need to reuse your line:
[jsonDictionary objectForKey:#"Success"]
try having a look at
[jsonDictionary objectForKey:#"Devices"]
You really need to figure out what type it returns.
If you're lucky, it returns an NSDictionary, or alternately something that you can easily turn into an NSDictionary.

Getting array elements with valueForKeyPath

Is there any way to access an NSArray element with valueForKeyPath? Google's reverse geocoder service, for example, returns a very complex data structure. If I want to get the city, right now I have to break it into two calls, like this:
NSDictionary *address = [NSString stringWithString:[[[dictionary objectForKey:#"Placemark"] objectAtIndex:0] objectForKey:#"address"]];
NSLog(#"%#", [address valueForKeyPath:#"AddressDetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.LocalityName"]);
Just wondering if there's a way to shoehorn the objectAtIndex: call into the valueForKeyPath string. I tried a javascript-esque formulation like #"Placemark[0].address" but no dice.
Unfortunately, no. The full documentation for what's allowed using Key-Value Coding is here. There are not, to my knowledge, any operators that allow you to grab a particular array or set object.
Here's a category I just wrote for NSObject that can handle array indexes so you can access a nested object like this: "person.friends[0].name"
#interface NSObject (ValueForKeyPathWithIndexes)
-(id)valueForKeyPathWithIndexes:(NSString*)fullPath;
#end
#import "NSObject+ValueForKeyPathWithIndexes.h"
#implementation NSObject (ValueForKeyPathWithIndexes)
-(id)valueForKeyPathWithIndexes:(NSString*)fullPath
{
NSRange testrange = [fullPath rangeOfString:#"["];
if (testrange.location == NSNotFound)
return [self valueForKeyPath:fullPath];
NSArray* parts = [fullPath componentsSeparatedByString:#"."];
id currentObj = self;
for (NSString* part in parts)
{
NSRange range1 = [part rangeOfString:#"["];
if (range1.location == NSNotFound)
{
currentObj = [currentObj valueForKey:part];
}
else
{
NSString* arrayKey = [part substringToIndex:range1.location];
int index = [[[part substringToIndex:part.length-1] substringFromIndex:range1.location+1] intValue];
currentObj = [[currentObj valueForKey:arrayKey] objectAtIndex:index];
}
}
return currentObj;
}
#end
Use it like so
NSString* personsFriendsName = [obj valueForKeyPathsWithIndexes:#"me.friends[0].name"];
There's no error checking, so it's prone to breaking but you get the idea.
You can intercept the keypath in the object holding the NSArray.
In your case the keypath would become Placemark0.address... Override valueForUndefinedKey; look for the index in the keypath; something like this:
-(id)valueForUndefinedKey:(NSString *)key
{
// Handle paths like Placemark0, Placemark1, ...
if ([key hasPrefix:#"Placemark"])
{
// Caller wants to access the Placemark array.
// Find the array index they're after.
NSString *indexString = [key stringByReplacingOccurrencesOfString:#"Placemark" withString:#""];
NSInteger index = [indexString integerValue];
// Return array element.
if (index < self.placemarks.count)
return self.placemarks[index];
}
return [super valueForUndefinedKey:key];
}
This works really well for model frameworks e.g. Mantle.
Subclass NSArrayController or NSDictionaryController
Use NSArrayController for this purpose, because NSObjectController does not include NSArrayController's provided handling of changes to bound array elements. If you use this same code with NSObjectController instead, then using Cocoa Bindings with your NSObjectController instance will only set the (bound interface element's) value at the time of binding but will not receive the messages from array elements in return. By using NSObjectController for this purpose, the user interface will not continue to update even though the contentObject is updated. Simply use the same code with NSArrayController to also include proper support for arrays -- which is the matter at hand.
#import <Cocoa/Cocoa.h>
#interface DelvingArrayController : NSArrayController
#end
#import "DelvingArrayController.h"
#implementation DelvingArrayController
-(id)valueForKeyPath:(NSString *)keyPath
{
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"^(.+?)\\[(\\d+?)\\]$" options:NSRegularExpressionCaseInsensitive error:&error];
NSArray<NSString*> *components = [keyPath componentsSeparatedByString:#"."];
id currentObject = self;
for (NSUInteger i = 0; i < components.count; i++)
{
if (![components[i] isEqualToString:#""])
{
NSTextCheckingResult *check_result = [regex firstMatchInString:components[i] options:0 range:NSMakeRange(0, components[i].length)];
if (!check_result)
currentObject = [currentObject valueForKey:components[i]];
else
{
NSRange array_name_capture_range = [check_result rangeAtIndex:1];
NSRange number_capture_range = [check_result rangeAtIndex:2];
if (number_capture_range.location == NSNotFound)
currentObject = [currentObject valueForKey:components[i]];
else if (array_name_capture_range.location != NSNotFound)
{
NSString *array_name = [components[i] substringWithRange:array_name_capture_range];
NSUInteger array_index = [[components[i] substringWithRange:number_capture_range] integerValue];
currentObject = [currentObject valueForKey:array_name];
if ([currentObject count] > array_index)
currentObject = [currentObject objectAtIndex:array_index];
}
}
}
}
return currentObject;
}
//at some point... also override setValueForKeyPath :-)
#end
This code uses NSRegularExpression, which is for macOS 10.7+.
I leave it as an exercise for you to use the same approach to also override setValueForKeyPath, if you want write functionality.
Cocoa Bindings Example Usage
Say we want a little trivia game, with a window that shows a question and uses four buttons to display multiple-choice options. We have the questions and multiple-choice options as NSStrings in a plist, and also an NSNumber or optionally BOOL entries to indicate the correct answers. We want to bind the option buttons to options in the array, for each question also stored in an array.
Here is the example plist containing some trivia questions related to the game Halo. Notice that the options are located within nested arrays.
In this example, I use NSObjectController *stringsController as the controller for the entire plist file, and DelvingArrayController *triviaController as the controller for the trivia-related plist entries. You might simply use one DelvingArrayController instead, but I provide this for your understanding.
The trivia window is really simple, so I merely design it using Interface Builder in MainMenu.xib:
A subclass of NSDocumentController is used for showing the trivia window via an NSMenuItem added in Interface Builder. The instance of this subclass is also in the .xib, so if we want to use the interface elements in the .xib, we have to wait for the Application Delegate instance's - (void)applicationDidFinishLaunching:(NSNotification *)aNotification method or otherwise wait until the .xib has finished loading...
#import <Cocoa/Cocoa.h>
#import "MenuInterfaceDocumentController.h"
#interface AppDelegate : NSObject <NSApplicationDelegate>
#property IBOutlet MenuInterfaceDocumentController *PrimaryInterfaceController;
#end
#import "AppDelegate.h"
#interface AppDelegate ()
#end
#implementation AppDelegate
#synthesize PrimaryInterfaceController;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
if ([NSApp mainMenu])
{
[PrimaryInterfaceController configureTriviaWindow];
}
}
#import <Cocoa/Cocoa.h>
#interface MenuInterfaceDocumentController : NSDocumentController
{
IBOutlet NSMenuItem *MenuItemTrivia; // shows the Trivia window
IBOutlet NSWindow *TriviaWindow;
IBOutlet NSTextView *TriviaQuestionField;
IBOutlet NSButton *TriviaOption1, *TriviaOption2, *TriviaOption3, *TriviaOption4;
}
#property NSObjectController *stringsController;
-(void)configureTriviaWindow;
#end
#import "MenuInterfaceDocumentController.h"
#interface MenuInterfaceDocumentController ()
#property NSDictionary *languageDictionary;
#property DelvingArrayController *triviaController;
#property NSNumber *triviaAnswer;
#end
#implementation MenuInterfaceDocumentController
#synthesize stringsController, languageDictionary, triviaController, triviaAnswer;
// all this happens before the MainMenu is available, and before the AppDelegate is sent applicationDidFinishLaunching
-(instancetype)init
{
self = [super init];
if (self)
{
if (!stringsController)
stringsController = [NSObjectController new];
stringsController.editable = NO;
// check for the plist file, eventually applying the following
languageDictionary = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"en" ofType:#"plist"]];
if (languageDictionary)
[stringsController setContent:languageDictionary];
if (!triviaController)
{
triviaController = [DelvingArrayController new];
[triviaController bind:#"contentArray" toObject:stringsController withKeyPath:#"selection.trivia" options:nil];
}
triviaController.editable = NO;
if (!triviaAnswer)
{
triviaAnswer = #0;
[self bind:#"triviaAnswer" toObject:triviaController withKeyPath:#"selection.answer" options:nil];
}
}
return self;
}
// if we ever do something like change the plist file to a duplicate plist file that is in a different language, use this kind of approach to keep the same trivia entry active
-(IBAction)changeLanguage:(id)sender
{
NSUInteger triviaQIndex = triviaController.selectionIndex;
if (sender == MenuItemEnglishLanguage)
{
if ([self changeLanguageTo:#"en" Notify:YES])
{
[self updateSelectedLanguageMenuItemWithLanguageString:#"en"];
if ([triviaController.content count] > triviaQIndex) // in case the plist files don't match
[triviaController setSelectionIndex:triviaQIndex];
}
else
[self displayAlertFor:CUSTOM_ALERT_TYPE_LANGUAGE_CHANGE_FAILED];
}
else if (sender == MenuItemGermanLanguage)
{
if ([self changeLanguageTo:#"de" Notify:YES])
{
[self updateSelectedLanguageMenuItemWithLanguageString:#"de"];
if ([triviaController.content count] > triviaQIndex)
[triviaController setSelectionIndex:triviaQIndex];
}
else
[self displayAlertFor:CUSTOM_ALERT_TYPE_LANGUAGE_CHANGE_FAILED];
}
}
-(void)configureTriviaWindow
{
[TriviaQuestionField bind:#"string" toObject:triviaController withKeyPath:#"selection.question" options:nil];
[TriviaOption1 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[0]" options:nil];
[TriviaOption2 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[1]" options:nil];
[TriviaOption3 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[2]" options:nil];
[TriviaOption4 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[3]" options:nil];
}
// this method is how you would manually set the value if you did not use binding:
-(void)updateTriviaAnswer
{
triviaAnswer = [triviaController valueForKeyPath:#"selection.answer"];
}
-(IBAction)changeTriviaQuestion:(id)sender
{
if (triviaController.selectionIndex >= [(NSArray*)triviaController.content count] - 1)
[triviaController setSelectionIndex:0];
else
[triviaController setSelectionIndex:(triviaController.selectionIndex + 1)];
}
-(IBAction)showTriviaWindow:(id)sender
{
[TriviaWindow makeKeyAndOrderFront:sender];
}
- (IBAction)TriviaOptionChosen:(id)sender
{
// tag integers 0 through 3 are assigned to the option buttons in Interface Builder
if ([sender tag] == triviaAnswer.integerValue)
[self changeTriviaQuestion:sender];
else
NSBeep();
}
#end
Summary of Sequence
NSObjectController *stringsController = [[NSObjectController alloc] initWithContent:[NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"en" ofType:#"plist"]]];
DelvingArrayController *triviaController = [DelvingArrayController new];
[triviaController bind:#"contentArray" toObject:stringsController withKeyPath:#"selection.trivia" options:nil];
NSNumber *triviaAnswer = #0;
[self bind:#"triviaAnswer" toObject:triviaController withKeyPath:#"selection.answer" options:nil];
// bind to .xib's interface elements after the nib has finished loading, else the IBOutlets are null
[TriviaQuestionField bind:#"string" toObject:triviaController withKeyPath:#"selection.question" options:nil];
[TriviaOption1 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[0]" options:nil];
[TriviaOption2 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[1]" options:nil];
[TriviaOption3 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[2]" options:nil];
[TriviaOption4 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[3]" options:nil];
// when the user chooses the correct option, go to the next question
if ([sender tag] == triviaAnswer.integerValue)
{
if (triviaController.selectionIndex >= [(NSArray*)triviaController.content count] - 1)
[triviaController setSelectionIndex:0];
else
[triviaController setSelectionIndex:(triviaController.selectionIndex + 1)];
}
Create methods that supports array for NSObject:
#interface NSObject(ArraySupported)
-(id)valueForKeySupportedArray:(NSString*)path;
-(id)valueForKeyPathSupportedArray:(NSString*)fullPath;
#end
#implementation NSObject(ArraySupported)
-(id)valueForKeySupportedArray:(NSString*)path {
id value = nil;
if ([self isKindOfClass:[NSArray class]]) {
NSArray *array = (NSArray *)self;
NSUInteger index = path.integerValue;
if (index >= 0 && index < array.count) {
value = array[index];
}
} else {
value = [self valueForKey:path];
}
return value;
}
-(id)valueForKeyPathSupportedArray:(NSString*)fullPath {
NSArray* parts = [fullPath componentsSeparatedByString:#"."];
id value = self;
for (NSString* part in parts) {
value = [value valueForKeySupportedArray:part];
if (value == nil) {
break;
}
}
return value;
}
#end
How to use:
NSObject *object = #{#"Placemark":#[#{#"address":#"..."}]};
NSString *address = [object valueForKeyPathSupportedArray:#"Placemark.0.address"];
// address = "..."