Escribiendo

un poquito

Making Mantle Deserialization Generic

The following discussion is geared towards Cocoa developers who have been using Mantle for some time. For those who haven’t, Mantle is a model framework for Cocoa and Cocoa Touch or, in simpler terms, is a framework that allows you to parse JSONs into objects and also serialize those objects back to JSON.

If you have been been working with Mantle, you’ve most likely been writing code like this:

...
[SomeNetworkingClass loadUserWithCompletion:^(NSURLResponse *response,
                                              id JSONObject,
                                              NSError *error) {
    if (!error) {
      NSError *mantleError;
      User* newUser = [MTLJSONAdapter modelOfClass:[User class]
                                fromJSONDictionary:JSONObject
                                             error:&mantleError];
      if (!mantleError) {
        //Do something with user
        ...
        } else {
          //Handle Mantle error
          ...
        }
        } else {
          //Handle network error
          ...
        }
        }];
...

You get the data from some service, test for errors (you check for errors right?), and finally use Mantle to parse the JSON (object or collection) to one of your model objects. This works great, but it tends to quickly create a lot of noise in the object responsible for this task.

At Prolific Interactive we’ve been experimenting with a generic block that we refer to as MantleBlock. The purpose of this block is to reuse the parsing code that we would otherwise be repeating all around.

Let’s do an overview of the method that generates the block:

+ (MantleBlock)jlm_parseBlockWithClass:(Class)className
                        firstCompletion:(void (^)(id items,NSError* error))firstCompletion
                       secondCompletion:(void (^)(id items,NSError* error))secondCompletion {

      NSParameterAssert(className);
      NSParameterAssert(firstCompletion || secondCompletion);
...

The returned MantleBlock is a typedef block to improve the readability: typedef void (^MantleBlock)(id, id, NSError*);. The important point here is that this method signature matches the one used in the callbacks of the backing object used to retrieve the data. In our case this is a AFHTTPSessionManager subclass with modified callbacks.

The method takes three parameters: a class object that must be a MTLModel subclass, since it will be used to determine the model object to parse the JSON response to, and two blocks with the same signature–firstCompletion and secondCompletion.

Finally, we assert the parameters, as this is standard practice on our code and this block is no exception.

MantleBlock newParseBlock = ^(id response,
                              id responseObject,
                              NSError* networkError) {
    if (networkError) {
      if (firstCompletion) {
        firstCompletion(nil, networkError);
      }

      if (secondCompletion) {
        secondCompletion(nil, networkError);
      }

      return;
    }

...

We now declare the MantleBlock that we’ll be returning. This block will be called by our AFHTTPSessionManager subclass and hence we have to check for a networkError. Then, if a firstCompletion block was provided we call it with a nil set of items and the networkError. We do the same for the secondCompletion block and then return. This example is what we’ll be doing for most of the rest of the block, as you’ll see.

...
//The caller doesn't expect or doesn't want the response parsed that
if (className == [NSNull class]) {
    if (firstCompletion) {
      firstCompletion(responseObject, nil);
    }

    if (secondCompletion) {
      secondCompletion(responseObject, nil);
    }

    return;
  }
...

This covers the case in which the caller doesn’t need or want the response object parsed.

...
NSAssert([responseObject isKindOfClass:[NSArray class]] || [responseObject isKindOfClass:[NSDictionary class]],
         @"JSON objects are either Arrays or Dictionaries");
  ...

The assertion is pretty much self-explanatory.

...
  NSError *mantleError = nil;
  id parsedModelOfClass = nil;
  if ([responseObject isKindOfClass:[NSArray class]]) {
    parsedModelOfClass = [MTLJSONAdapter modelsOfClass:className
                                         fromJSONArray:responseObject
                                                 error:&mantleError];
    } else if ([responseObject isKindOfClass:[NSDictionary class]]) {
      parsedModelOfClass = [MTLJSONAdapter modelOfClass:className
                                     fromJSONDictionary:responseObject
                                                  error:&mantleError];
    }
...

Continuing from the assertion above, we parse the JSON object as a JSON Array or as JSON Dictionary. The goal is for the block to do the right thing without the caller providing that information.

...
if (mantleError) {
  if (firstCompletion) {
    firstCompletion(nil, mantleError);
  }

  if (secondCompletion) {
    secondCompletion(nil, mantleError);
  }

  return;
}
...

We do the callback dance again, but for any errors that happened during parsing.

...
    //No errors occurred. Call the dataStore block first if any.
    if (firstCompletion) {
      firstCompletion(parsedModelOfClass, nil);
    }

    if (secondCompletion) {
      secondCompletion(parsedModelOfClass, nil);
    }
  };

  return newParseBlock;
}

Everything went well if we reached this far in the block code so we do the callback dance once more time, but for the only success case and, finally, return the MantleBlock to the caller.

The code above was modified to simplify the discussion, but the complete version is here. Also, there’s a recording of a related lighting talk: Reusing Mantle Blocks - Mobile Lightning Talk