My favorite hack with the Objective-C Runtime

The Objective-C runtime allows you to look under the hood of your code and play with it while it is being executed. You can see the properties and methods of a class, it’s super class, and the methods and properties from a protocol. You can even change the way your program works at runtime by creating a new class, changing it’s super class or swizzling it’s methods.
After about two years of iOS development I really got tired of integrating web services into my application. If a login request succedes it would return something similar to this response:

{
  "status" : "success",
  "user"   : {
    "first_name" : "Andrei",
    "last_name" : "Puni",
      "age" : 22
  }
}

After handling the networking part we usually get a NSDictionary with the values from the json/xml respose. Now all we need to do is write the User class and create a User instance from that :

// User.h

@interface User : NSObject

@property (nonatomic, retain) NSString *firstName;
@property (nonatomic, retain) NSString *lastName;

@property (nonatomic, retain) NSNumber *age;

+ (instancetype)userFromInfo:(NSDictionary *)info;

@end
// User.m

@implementation User

+ (instancetype)createFromInfo:(NSDictionary *)info {
    User *user = [self new];

    user.firstName = info[@"first_name"];
    user.lastName = info[@"last_name"];
    user.age = info[@"age"];

    return user;
}

@end

This code is pretty simple and does the job, but it has some issues:

  • if you add or remove a property from the model you must also mirror the changes in the userFromInfo method
  • you have to write code everytime you create a new model

If we would have a NSArray with the name of the properties of the model (for User it would be@[@"firstName", @"lastName", @"age"]) this code would be a bit different:

// User.m

// code from: http://stackoverflow.com/questions/1918972/camelcase-to-underscores-and-back-in-objective-c
NSString *CamelCaseToUnderscores(NSString *input) {
    NSMutableString *output = [NSMutableString string];
    NSCharacterSet *uppercase = [NSCharacterSet uppercaseLetterCharacterSet];
    for (NSInteger idx = 0; idx < [input length]; idx += 1) {
        unichar c = [input characterAtIndex:idx];
        if ([uppercase characterIsMember:c]) {
            [output appendFormat:@"%s%C", (idx == 0 ? "" : "_"), (unichar)(c & ~0×20)];
        } else {
            [output appendFormat:@"%C", c];
        }
    }
    return output;
}

@implementation User

- (NSArray *)propertyList {
    return @[@"firstName", @"lastName", @"age"];
}

+ (instancetype)createFromInfo:(NSDictionary *)info {
    NSObject *object = [self new];

    for (NSString *property in object.propertyList) {
        id value = info[property];
        // look for the underscore form "firstName" -> "first_name" 
        if (value == nil) {
            value = info[CamelCaseToUnderscores(property)];
        }
        [object setValue:value
                  forKey:property];
    }

    return object;
}

@end

All we have to do now is get the class property list and move this code in a category for NSObject and never write the same code again.

#import <objc/runtime.h>

...

- (NSArray *)propertyList {
    Class currentClass = [self class];

    NSMutableArray *propertyList = [NSMutableDictionary array];
    // class_copyPropertyList does not include properties declared in super classes
    // so we have to follow them until we reach NSObject
    do {
        unsigned int outCount, i;
        objc_property_t *properties = class_copyPropertyList(currentClass, &outCount);

        for (i = 0; i < outCount; i++) {
            objc_property_t property = properties[i];

            NSString *propertyName = [NSString stringWithFormat:@"%s", property_getName(property)];

            [propertyList addObject:propertyName];
        }
        free(properties);
        currentClass = [currentClass superclass];
    } while ([currentClass superclass]);

    return propertyInfo;
}

Now that we have this magic method, the code will look like this:

// User.
@interface User : NSObject

@property (nonatomic, retain) NSString *firstName;
@property (nonatomic, retain) NSString *lastName;

@property (nonatomic, retain) NSNumber *age;

@end

...

// User.m

@implementation User

@end

...

AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:@"http://example.com/login.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
    User *user = [User createFromInfo:responseObject];

    // do something with user 

} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    NSLog(@"Error: %@", error);
}];

The reverse function is easy to implement and allows (de)serializing almost any object (as long as all the properties are objects). You can find these methods with some optimizations and tweaks in my cocoapod. There you will also find a lot of convenience methods and other useful code snippets that I use in most of my projects.

Hope you liked the hack, I’m looking forward to seeing your thoughts in the comments!

Advertisements
Tagged with: , , , , , , , , , , ,
Posted in hack, ios, magic, objc, objective-c, runtime
8 comments on “My favorite hack with the Objective-C Runtime
  1. Your code is leaking memory. From Apple’s docs:

    Return Value
    An array of pointers of type objc_property_t describing the properties declared by the class. Any properties declared by superclasses are not included. The array contains *outCount pointers followed by a NULL terminator. You must free the array with free().

  2. Also, I would probably start the loop with:
    Class nsObjectClass = NSClassFromString(@”NSObject”) ;

    do {

    while (currentClass != nsObjectClass) ;

    This is safer than relying on the fact that NSObject doesn’t have a superclass. Nor does NSProxy, BTW, and I have been burnt once too many by making that assumption: people can decide to use their own root class, or even no root class at all.

  3. Also, your:

    (unichar)(c ^ 32)

    is too clever. It works because you know at that point that C is an uppercase letter and that 0x20 will flip the bit that says: “uppercase”.

    But it would be much clearer to say:
    (c & ~0x20)

    as you are not after flipping a bit (turning it to its opposite value) but after clearing it.

    The way to clear a bit (bit 5, 0b00100000) is by taking its one complement, the ~ operator (0b11011111) and applying the and operator, &

    • andrei512 says:

      I saw that “trick” in a topcoder solution and I’ve been using it ever since.
      The problem was to change all lowercase into uppercase and vice versa. The shortest solution looked like this:

      vector solve(vector message) {

      for (int i = 0; i < message.size(); ++i) message[i] ^= 32;

      return a;
      }

  4. Everyone says:

    Grammar fixes:

    > The Objective-c Runtime allows you to …

    Upper case ‘c’ and, in this context, lower case ‘r’.

    > while it is beening executed

    Should be spelled ‘being’.

    > it’s superclass, it’s subclasses … changing it’s super class or swizzling it’s methods.

    All of these should be ‘its’. ‘It’s’ is a contraction for ‘it is’ and ‘it has’.

    > tweeks

    Tweaks.

  5. […] is fun and cool, but Objc is a tank. Here is my favorite hack in objc adapted to […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: