15

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.


james Burns
  • 864
  • 2
  • 9
  • 22

6 Answers6

58

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);
Richard J. Ross III
  • 55,009
  • 24
  • 135
  • 201
  • Yeah, objc-runtime is definitely the way to go. – Matt Wilding Apr 25 '12 at 15:21
  • 2
    You could also just get the keys from the runtime and use `-dictionaryWithValuesForKeys:` from `NSKeyValueCoding`. Or you use that directly with a hard-coded list of keys, since maybe you don't want *every* property in the dictionary. – Ken Thomases Apr 25 '12 at 15:50
  • This should be the selected answer of this question. – Khaled Annajar Mar 31 '13 at 13:26
  • 1
    @khaled no, it shouldn't be. The accepted answer refers to the one which helped the OP the most, not the one thats most helpful to everyone. – Richard J. Ross III Mar 31 '13 at 14:36
  • @IlkerBaltaci I'm not quite sure what you mean. Yes, my solution will only go down 'one level', but if you are truly trying to serialize a class, you should be using a NSKeyedArchiver & the NSCoder protocols, which does have support for circular references and nested objects. – Richard J. Ross III Aug 28 '13 at 16:55
  • 1
    Remember to take care of nil values. Since you can't insert nil into a dictionary. – Fogh Mar 25 '14 at 07:37
  • @Fogh that edit was completely unnecessary, it changed absolutely nothing in the code, and appeared to be simply a way to gain reputation. That is strongly discouraged on SO, as it bumps the answer to the top of the active posts list needlessly. I have rolled the answer back to its original state, as frankly I didn't care for how you used spaces in the method signature. – Richard J. Ross III Mar 25 '14 at 07:54
11

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.

iHS
  • 5,372
  • 4
  • 31
  • 49
  • 1
    Hmm, interesting. Just note that this will also fail for co-dependent entries (e.g. Object `a` references Object `b` who references Object `a`, and so forth), . In that scenario, I would use `NSCoder` / `NSCoding` instead, though, as that was not really what this was intended for. – Richard J. Ross III Oct 15 '12 at 13:39
  • But in the case of you have a NSArray of a "classObject", it's not works. – Damien Romito Mar 07 '14 at 02:47
  • great answer, that worked for me! The only minor change I had to do was in the capitalizedString part: my class name was UserDevice so I had to change the method to capitalize the first letter only. – Alberto M Jun 22 '16 at 10:45
5

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"];
Paul.s
  • 38,494
  • 5
  • 70
  • 88
  • That's good to know. Yes, I named the properties the same. I was getting errors that the non-dictionary wouldn't respond to objectForKey, but didn't realize that valueForKey worked that way. Thanks! – james Burns Apr 25 '12 at 21:45
2

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];
}
Damien Romito
  • 9,801
  • 13
  • 66
  • 84
1

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
Pradeep Mahdevu
  • 7,613
  • 2
  • 31
  • 29
0

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];
Alexander Perechnev
  • 2,797
  • 3
  • 21
  • 35