[color=rgb(51,51,51)][font=Georgia]
Our latest game Skyward Slots makes extensive use of JSON. We send Gigabytes of it flying back and forth haphazardly between client and server over a WebSocket connection. At first, we wrote code by hand to pack and unpack each message. Later on we decided that life is too short for that.
[/font][/color][color=rgb(51,51,51)][font=Georgia]
In the beginning, we just dove into the JSON right where we needed it.
[/font][/color]-(void) updateUserInterface:(NSDictionary*)message{ topBar.coinsLabel.string = [[message objectForKey:@"coins"] stringValue];}
[color=rgb(51,51,51)][font=Georgia]
This isn't so great because it's a) quite verbose and b) difficult to change. If we change the name of the "coins" parameter to "cash", we'll have to search-and-replace and hope that we get every instance. There's no way to tell if we missed one; it will fail silently.
[/font][/color][color=rgb(51,51,51)][font=Georgia]
Our solution was to write a layer of model objects which process JSON dictionaries and expose the data via properties.
[/font][/color]@interface Product : NSObject@property BOOL scalable;@property (strong) NSString* productType;@property int quantity;@property (strong) SKProduct* skProduct;-(id)initWithDictionary:(NSDictionary*)dic;@end@implementation Product-(id)initWithDictionary:(NSDictionary *)dic{ if (self = [super init]) { self.scalable = [[dic objectForKey:@"scalable"] boolValue]; self.productType = [dic objectForKey:@"productType"]; self.quantity = [[dic objectForKey:@"quantity"] intValue]; } return self;}
[color=rgb(51,51,51)][font=Georgia]
Now if we change a property name, we'll get a bunch of compiler errors. This technique also gets us code completion, which is great.
[/font][/color][color=rgb(51,51,51)][font=Georgia]
The only downside is, writing these model classes gets old FAST. And they're so repetitive and simple, you'd think we could automate it somehow!
[/font][/color][color=rgb(51,51,51)][font=Georgia]
I sat down and wrote a gob of macros to do just that. Now we have one header file with all of our models, which now look like this:
[/font][/color]SBStruct(HSLoginResult) SBProperty(NSNumber, id) SBProperty(NSString, token) SBProperty(NSString, updateURL)SBEndStruct(HSLoginResult)
[color=rgb(51,51,51)][font=Georgia]
This generates an Objective C class which can be used like so:
[/font][/color]HSLoginResult* result = [HSLoginResult decode:dictionary];NSLog(@"%d", result.id.intValue);
[color=rgb(51,51,51)][font=Georgia]
Much better! The best part is, we still have code completion. We can also nest these structures.
[/font][/color][color=rgb(51,51,51)][font=Georgia]
So how does it work?
[/font][/color][color=rgb(51,51,51)][font=Georgia]
Here's the SBStruct and SBProxy macros:
[/font][/color]#define SBStruct(name) \@interface name : SBStructure#define SBProperty(type, name) \@property (strong, nonatomic) type* name;#define SBEndStruct(name) @end
[color=rgb(51,51,51)][font=Georgia]
This generates a valid class definition with all the right properties. Then in the implementation file, we include our definitions again and set up the macros to generate the actual packing/unpacking code:
[/font][/color]#define SBStruct(name) \@implementation name (Decode) \+(id) decode:(id)dict \{ \ if (dict == [NSNull null]) \ return nil; \ name* instance = [name new];#define SBProperty(type, name) \ instance.name = [type decode:[dict objectForKey:@#name]];#define SBEndStruct(name) \ return instance; \} \@end
[color=rgb(51,51,51)][font=Georgia]
The only thing left to do is to add categories to the basic data types to make them implement the decode: method, like so:
[/font][/color]@implementation NSNumber (Decode)+(NSNumber*) decode:(id)value{return value == [NSNull null] ? nil : (NSNumber*)value;}@end// and so on...
[color=rgb(51,51,51)][font=Georgia]