-
-
Save tapi/2489095 to your computer and use it in GitHub Desktop.
// | |
// NSObject+SMDictionaryMapping.h | |
// SoundTrack | |
// | |
// Created by Paddy O'Brien on 12-04-24. | |
// Copyright (c) 2012 Paddy O'Brien. All rights reserved. | |
// | |
#import <Foundation/Foundation.h> | |
@protocol SMMappedObject <NSObject> | |
+ (NSDictionary *)mappedKeys; | |
@end | |
@interface NSObject (SMDictionaryMapping) | |
- (id)initWithDictionary:(NSDictionary *)dictionary; | |
- (void)updateWithDictionary:(NSDictionary *)dictionary; | |
@end |
// | |
// NSObject+DictionaryMapping.m | |
// SoundTrack | |
// | |
// Created by Paddy O'Brien on 12-04-24. | |
// Copyright (c) 2012 Paddy O'Brien. All rights reserved. | |
// | |
#import "NSObject+DictionaryMapping.h" | |
@interface NSObject () | |
- (void)mapDictionaryToProperties:(NSDictionary *)dictionary; | |
@end | |
@implementation NSObject (DictionaryMapping) | |
- (id)initWithDictionary:(NSDictionary *)dictionary | |
{ | |
if (self) { | |
[self mapDictionaryToProperties:dictionary]; | |
} | |
return self; | |
} | |
- (void)updatePropertiesWithDictionary:(NSDictionary *)dictionary | |
{ | |
[self mapDictionaryToProperties:dictionary]; | |
} | |
- (void)mapDictionaryToProperties:(NSDictionary *)dictionary | |
{ | |
NSArray* keys = [dictionary allKeys]; | |
for (NSString *key in keys) { | |
NSString *mappedKey = key; | |
if ([self conformsToProtocol:@protocol(SMMappedObject)] && [self respondsToSelector:@selector(mappedKeys)]) { | |
NSDictionary *mappedKeys = [self performSelector:@selector(mappedKeys)]; | |
if ([mappedKeys valueForKey:key]) { | |
mappedKey = [mappedKeys valueForKey:key]; | |
} | |
} | |
[self setValue:[dictionary valueForKey:key] forKey:mappedKey]; | |
} | |
} | |
@end |
// | |
// NSObject+SMDictionaryMapping.h | |
// | |
// Created by Paddy O'Brien on 12-04-24. | |
// Copyright (c) 2012 Paddy O'Brien. All rights reserved. | |
// | |
#import <Foundation/Foundation.h> | |
@protocol SMMappedObject <NSObject> | |
+ (NSDictionary *)mappedKeys; | |
@end | |
@interface NSObject (SMDictionaryMapping) | |
- (id)initWithDictionary:(NSDictionary *)dictionary; | |
- (void)updateWithDictionary:(NSDictionary *)dictionary; | |
@end |
// | |
// NSObject+DictionaryMapping.m | |
// | |
// Created by Paddy O'Brien on 12-04-24. | |
// Copyright (c) 2012 Paddy O'Brien. All rights reserved. | |
// | |
#import "NSObject+DictionaryMapping.h" | |
@interface NSObject () | |
- (void)mapDictionaryToProperties:(NSDictionary *)dictionary; | |
@end | |
@implementation NSObject (DictionaryMapping) | |
- (id)initWithDictionary:(NSDictionary *)dictionary | |
{ | |
if (self) { | |
[self mapDictionaryToProperties:dictionary]; | |
} | |
return self; | |
} | |
- (void)updatePropertiesWithDictionary:(NSDictionary *)dictionary | |
{ | |
[self mapDictionaryToProperties:dictionary]; | |
} | |
- (void)mapDictionaryToProperties:(NSDictionary *)dictionary | |
{ | |
NSArray* keys = [dictionary allKeys]; | |
for (NSString *key in keys) { | |
NSString *mappedKey = key; | |
if ([self conformsToProtocol:@protocol(SMMappedObject)] && [self respondsToSelector:@selector(mappedKeys)]) { | |
NSDictionary *mappedKeys = [self performSelector:@selector(mappedKeys)]; | |
if ([mappedKeys valueForKey:key]) { | |
mappedKey = [mappedKeys valueForKey:key]; | |
} | |
} | |
id value = ([[dictionary valueForKey:key] isKindOfClass:[NSNull class]]) ? nil : [dictionary valueForKey:key]; | |
[self setValue:value forKey:mappedKey]; | |
} | |
} | |
@end |
Note: the problem this is designed to solve is initializing an object from JSON deserialized to an NSDictionary.
the whole mapped key nonsense is to handle cases where JSON propertied conflict with reserved words such as id.
Neat idea - this is something I've struggled with, too. It would also be cool to have something where NSNull
isn't (necessarily) successfully set as a property, since the dynamic runtime won't complain at the time, but sending the length
selector to NSNull
crashes your app later.
Something like this?
id value = ([[dictionary valueForKey:key] isKindOfClass:[NSNull class]]) ? nil : [dictionary valueForKey:key];
[self setValue:value forKey:mappedKey];
Yeah, I mean, NSNull
is supposed to be a sentinel, but using it kind of sucks (imo). I like this solution, since for all cases but an NSMutableDictionary
, nil is going to be fine. Are you planning to implement setValue:forUndefinedKey:
?
Im of two minds. On one hand Id like the code to run even in the event of errors. On the other hand if there's an error in data modelling between classes and JSON responses id like it to fail fast so its more likely to be caught during testing.
It's not a whole lot different than implementing the method once per class and doing the mapping manually except now it'll crash if you send it the wrong dictionary instead of just giving you and empty or incorrectly populated object.
Frig I just realized that this wont handle nested objects.
Could use reflection in setValue:forKey:
or the property setter.
Could you write up a quick snippet that uses this? I think I see the general idea, but I'm having trouble stepping through the code. I'll admit, that might be because I just woke up.
Well, your current approach would work except if you're being passed in a sub-entity (like the user model of a photo model). So you could do something like this:
-(void)setUserModel:(id)theUserModel
{
if ([theUserModel isKindOfClass:[NSDictionary class]])
{
self.userModel = [[UserModel alloc] initWithDictionary:theUserModel];
return;
}
_userModel = theUserModel;
}
Thoughts?
Yes but what if you actually want a dictionary as a property on an object? Interogate the class of the property to see if it conforms to the protocol perhaps?
@warwick an example might look like this
@interface Contact : NSObject <SMMappedObject>
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) Email *email;
@end
Later we want to get a bunch of contacts from some webservice that spits back JSON
- (NSArray *)getMyContacts
{
NSData *JSONData = [MagicWebRequestThatRequiresNoSetup GetURL:@"www.myaddressbook.com/contacts"];
NSArray *contacts = [JSONData parseObjectFromJSON];
NSMutableArray* retval = [NSMutableArray array];
for (NSDictionary *contactData in contact) {
Contact *newContact = [[Contact alloc] initWithDictionary:contactData];
[retval addObject:newContact];
}
return retval;
}
The idea is to have you local domain object populated without having to slog through writing your own mappings.
I'm curious about sub-entities and arrays of sub-entities, too. Would it be best to define those in the model class as an array of strings (JSON key names) you want explicitly mapped to sub-entities? One thing all of my JSON keys have in common is that the sub-entity keys are all upper-case first character. This could be the "auto" mode; if a mapped key is upper-case first character it looks for a model class of same name and maps accordingly. I haven't looked to see what something like RestKit does. Just seems like something that could be a single NSObject category rather than a giant library.
Not tested, just a proof of concept.