Alternative network response handling in Objective-C

For the latest iOS app we built at Smashing Boxes, I wanted to explore a new idea for handling JSON network responses.

A common method is to turn a JSON response into an equivalent object subclass. So in a social media app, if you have a request to /users/:id that returns a JSON response formed like this:

{ "username": "bob", "email": "bob@site.com", "id": 7 }

You would traditionally create an objective-C User object:

@interface User : NSObject
@property (strong, nonatomic) NSString *username;
@property (strong, nonatomic) NSString *email;
@property (strong, nonatomic) NSNumber *userID; // 'id' is reserved, we'd need to explicitly handle this in `setValue:forKey:`
- (id) initWithDictionary:(NSDictionary*)dictionary;
@end

Initialization for this object would likely call [self setValuesForKeysWithDictionary:dictionary]; to map the values of the dictionary to the objc properties.

One downside of this is that if there are a large number of different types of objects you wind up with tons of network-backed object classes to manage. With a complex api, the surface area can get large. Creating and maintaining all of those different objects just sounded very tedious, and I wanted to see if I could find another way.

For applications where the response properties don't have to become database relationships or anything, I think I've developed a pretty good alternative approach which is more lightweight. Instead, for each object that is returned by the API, we created protocols that can be assigned to the NSDictionaries that are converted directly from JSON. For the same JSON response above, you would have a corresponding protocol defined as:

@protocol ResponseUser <NSObject>
- (NSString*) username;
- (NSString*) email;
- (NSNumber*) identifier; // id is a reserved keyword, we can easily use a different, objc-friendly name
@end

So the idea is that everything remains an NSDictionary. Nothing is converted to custom object subclasses. This means that at any time, wherever this dictionary goes, we can print out its description and we'll see exactly what we received from the API. Even nested dictionaries can conform to protocols, so you could have the following response that defines a 'follow' action:

@protocol ResponseFollow <NSObject>
- (NSDictionary<ResponseUser>*) follower;
- (NSDictionary<ResponseUser>*) followed;
@end

This specifies a JSON response object that has other objects embedded in it. If we were using the Objective-C method each of these internal dictionaries would be converted to User objects, but since we know they conform to our protocol, we can just cast them with the protocol and keep using the NSDictionaries as-is, allowing us to access all the members from the top-level dictionary, such as follow.follower.username.

To make this work, we must specify a category on NSDictionary to implement all these protocols. For now the interface will be empty.

@interface NSDictionary (ResponseDict)
@end

And internally the implementation should just respond to the messages defined in the protocols. This turns out to be very concise and just gives us the value for the dictionary key that we expect to be there:

@implementation NSDictionary (ResponseDict)
- (NSString*) username { return self[@"username"]; }
- (NSString*) email { return self[@"email"]; }
- (NSNumber*) identifier { return self[@"id"]; }
- (NSDictionary<ResponseUser>*) follower { return self[@"follower"]; }
- (NSDictionary<ResponseUser>*) followed { return self[@"followed"]; }
@end

You can also see that the same category implements all of the keys -- the style is concise enough to support this; on our app with 30+ different response types, our header that defines all the protocols is under 300 lines and our implementation is under 150 lines. I find this much more maintainable than 30 different classes. Note that since the category doesn't declare these methods publicly we won't be able to call .username on just any NSDictionary in the app, we would need to first cast the dictionary to conform to the protocol (though -respondsToSelector:@selector(username) would return YES without casting). Many protocols share one implementation: the category implements the methods and the protocols are the interface to the methods.

One thing not addressed yet is mutability. If we need to create or modify a dictionary, we need to add a method to support that. I think this is the weakest link in this idea, but it turns out to be fairly manageable. We can just add a method on the category interface:

/** @return a modified NSDictionary<PROTOCOL> */
- (id) replaceResponseKey:(NSString*)internalKey withObject:(id)object;

With the following implementation:

- (id) replaceResponseKey:(NSString*)internalKey withObject:(id)object
{
    NSMutableDictionary *mutable = [self mutableCopy];
    if (object != nil) {
        mutable[internalKey] = object;
    } else {
        [mutable removeObjectForKey:internalKey];
    }
    return [NSDictionary dictionaryWithDictionary:mutable];
}

This method takes an NSString for the key. To manage this, I created a helper class called ResponseMutator, which implements class methods to do the modifications. For example,

NSDictionary<ResponseUser> *modifiedUser = 
[ResponseMutator user:user setUsername:username]

would internally call [user replaceResponseKey:@"user" withObject:newusername]. This keeps the hardcoded strings located in a maximum of two places (The NSDictionary category implementation and ResponseMutator) and allows us to use and change the response properties throughout the app without directly referencing the NSDictionary keys. Another benefit is that [ResponseMutator user... is greppable so it's easy to see all the places in the app where a user is modified.

It's also important for this concept to work well with the network methods. We're using AFNetworking which plays nicely with this whole idea because JSON gets turned into NSDictionaries that are passed into the success block. It seemed like it would be nice if the API manager could define the response protocols it expects each JSON response to conform to.

So it seemed like a good idea to typedef a block that defines each type of NSDictionary protocol we expect to receive. We wrote a macro to do this, because the macro makes the list of types more readable and also ensures all the typedefs are formed the same way:

#define GenerateSuccessBlockType(name, responseObjectType) \
    typedef void (^name)(NSURLSessionDataTask *task, responseObjectType responseObject);

This macro takes two parameters: the name for the typedef and the response object type. Usage of the macro looks like this:

GenerateSuccessBlockType( RequestSuccessUser, NSDictionary<ResponseUser>* );

Now our API manager stays readable when we use the RequestSuccessUser type in the method signature:

- (void)getUserWithID:(NSNumber*)userID success:(RequestSuccessUser)success;

And usage of this method will give us a dictionary with the specified protocol, allowing us to get the username, email, and identifier using dot-syntax:

    [[APIManager sharedManager] 
     getUserWithID:userID 
     success:^(NSURLSessionDataTask *task, NSDictionary<ResponseUser> *responseObject) {
      NSLog(@"name: %@, email: %@ id: %d", 
      responseObject.username, 
      responseObject.email
      responseObject.identifier.intValue);
    }];

That summarizes the basic idea -- it's a different method of handling network responses than I've encountered in objective-c before, and it does come with some pro's and con's, but overall I've found it much more pleasant to work with.

The Caveats

Though I like this method overall, there are a coulple problems that I can see with this method:

  • The protocols that declare the methods aren't explicitly associated with the NSDictionary category that implements the methods, though the association can be suggested by code proximity and likeness.
  • Wherever the NSDictioanry category gets imported, for any given NSDictionary asking the question [dict respondsToSelector:@selector(username)] will return YES, even performing the selector will just return nil, and only dictionaries declaring that they conform to NSDictionary<ResponseUser> will be able to call dict.username.
  • Nothing enforces that the ResponseMutator is the only way to modify the dictionaries, it's possible to break the ResponseMutator suggestion and just modify a dictionary in any way you want. If that happens, it might be difficult to track it down, because it would look like ordinary dictionary manipulation and so might not be highly greppable.

Comments:

rss

blog powered by Pelican