iOS App Architecture, Part 2: Data parsing

Last time we set our project, this time we look at how we could create a simple yet flexible architecture for our model layer.

Design

What I like to do is create a modular architecture, one in which I can add new types of objects without modifying the core of the app, that way I can expand the application without much fuss.

Since UI will come later in this series, I'll write this part using tests.

Choices

Let's start with a few choices before we design the architecture:

  • KZPropertyMapper for data mapping - because it's simple.
  • CoreData for data persistence - because it's a very common need/choice in commercial apps.
  • mogenerator - as it's useful for creating human / machine files
  • Magical Record - since we already know CoreData let's use a popular wrapper to help us out.
  • AFNetworking - a very popular library that I'll use for fetching data.
  • OHHTTPStubs - this is not backend tutorial, so we'll stub server responses.
  • Kiwi for tests -my favorite.

I'm not a huge fan of CoreData, but It gives me a nice way to have separation of concern.
When I use a Fetched Results Controller my UI can be automatically updated and I end up having something like this:

{{< photo src="/2014/04/Flow.png" width="400" height="57" >}}

In this article I'll talk about the data path.
Grab source code from GitHub

Implementation

First I start by setting up a project with crafter

1. KZBootstrap
2. KZBootstrapTests
Which target should I use for default?
1
1. KZBootstrap
2. KZBootstrapTests
Which target should I use for tests?
2
do you want to add networking? [y/n]
y
do you want to add coredata? [y/n]
y
do you want to add kiwi? [y/n]
y
duplicating configurations
setting up variety of options
preparing git ignore
preparing pod file
adding scripts
Finished.

Using crafter I now have all of my preferred libraries and custom warning levels without wasting time doing manual configuration.

Fetching

We won't be using a real backend server, instead we'll use the OHHTTPStubs library to stub network requests and return canned JSON responses. The library also provides us with simulated network speed (How cool is that?).

For this part we can get away with an extra simple DataProvider concept, it just needs to satisfy a few simple requirements:

  • it should allow stubbing fake data for an arbitrary URL (because we don't have backend)
  • it should return operations that can be cancelled

We can write tests for these 2 conditions like this:

it(@"should fetch data from an arbitrary URL", ^() {
  __block BOOL success = NO;
  [sut dataForURL:[NSURL URLWithString:@"http://fake.url"] withSuccessBlock:^(id responseData) {
    success = YES;
  } andFailureBlock:^(NSError *error) {
  }];

  [[expectFutureValue(@(success)) shouldEventually] beTrue];
});

it(@"should return operations that can be canceled", ^() {
  __block BOOL executed = NO;
  id<KZBCancelableOperation> operation = [sut dataForURL:[NSURL URLWithString:@"http://fake.url"] withSuccessBlock:^(id responseData) {
    executed = YES;
  } andFailureBlock:^(NSError *error) {
    executed = YES;
  }];

  [operation cancel];
  [[expectFutureValue(@(executed)) shouldNotEventually] beTrue];
});

In the first test we only care that it succeeds, in the second test I'm making sure that the operation can be cancelled.

Now to make the first test pass, we want to stub our network requests by using OHHTTPStubs:

- (void)setupStubs
{
  [OHHTTPStubs removeAllStubs];
  [self.mapping enumerateKeysAndObjectsUsingBlock:^(NSString *urlPath, NSString *fileName, BOOL *stop) {
    [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
      BOOL equal = [request.URL.absoluteString isEqualToString:urlPath];
      return equal;
    } withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) {
      OHHTTPStubsResponse *const response = [OHHTTPStubsResponse responseWithFileAtPath:OHPathForFileInBundle(fileName, nil) statusCode:200 headers:@{@"Content-Type" : @"text/json"}];
      response.responseTime = OHHTTPStubsDownloadSpeed3G;
      return response;
    }];
  }];
}

Here I setup all the stubbed network responses by using a mapping dictionary in form URLPath : FileName.

I also setup the stub to simulate the speed of a 3G connection since I want real async tests as we don't currently have a real UI

(In most cases you want to make tests run as fast as possible so that whole test-suite takes the minimum amount of time, since you'll be running them very often).

Implementing dataForURL is easy enough:

- (id <KZBCancelableOperation>)dataForURL:(NSURL *)url withSuccessBlock:(KZBSuccessBlock)successBlock andFailureBlock:(KZBFailureBlock)failureBlock
{
  id requestOperation = [[AFHTTPRequestOperationManager manager] GET:url.absoluteString parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
    successBlock(responseObject);
  } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    if(operation.isCancelled) {
      return;
    }
    failureBlock(error);
  }];
  NSAssert([requestOperation respondsToSelector:@selector(cancel)], @"Returned operation doesn't support KZBCancelableOperation protocol");
  return requestOperation;
}

Here I am using AFNetworking to do a simple fetch.

  • I don't want a cancelled action to callback my completion blocks so I ignore cancelled operations with a guard clause.
  • I want to make sure requestOperation supports canceling.
  • Since I don't have any value in exposing AFNetworking internal classes to my code I'll be using my cancelable protocol.

Parsing data

When it comes to parsing data, I'd like to make sure this implementation handles a few requirements:

  1. Easy to modify source format, if I need to change from JSON to something else, then that shouldn't be a massive endeavor.
  2. I'd like to be able to add new types of objects without modifying the parsing core. Ideally I would just add a class and have it working.
  3. Parsing should be very simple, preferably with little-to-no code.

Let's look how we can create a design that will take care of all of these requirements:

Source format

This requirement is very simple to satisfy since we'll be using KZPropertyMapper for mapping.

It doesn't rely on JSON/XML or any other format. It uses native data structures, so the only thing we need to make sure of is that our parser converts the source format into native types before passing the data to KZPropertyMapper.

Supporting new classes

This is the requirement that affects the design of this architecture the most.

I want to drop-in a new class to the project, and 'somehow' have it picked up by the parser and handled.

Protocol
@protocol KZBParsableObjectProtocol <NSObject>
@required
+ (NSString*)serverType;
+ (NSString*)serverIDPropertyName;
- (void)updateFromDictionary:(NSDictionary *)dictionary;
@end

Here we decide on 3 main features:

  1. We can define our own mapping for serverTypes, doing ORM with 1:1 naming is usually ugly, especially if you have to support multiple platforms.
  2. We select which property will be used for mapping serverTypes.
  3. Each object knows how to update itself from an NSDictionary*. In proper projects this might be a good place to return an error at the core parsing level, especially since KZPropertyMapper can generate validation errors for you.

It's a nice simple protocol, but it doesn't help us automatically support any new classes.

Finding parsable classes

It's actually quite simple to use the Obj-C runtime to get a list of all classes that conform to our protocol.

Let's start with being able to find classes conforming to an arbitrary protocol:

 it(@"should be able to find classes conforming to specific protocol", ^() {
  [[[KZBParsingHelper findClassesConformingToProtocol:@protocol(KZBParsableObjectProtocol)] should] equal:@[KZBTestParsableClass.class]];
});

In order to make this simple test pass, we need to get our hands a little bit dirty and use the straight C API of the Obj-C runtime.

+ (NSArray *)findClassesConformingToProtocol:(Protocol *)protocol
{
//! 1
  int numberOfClasses = objc_getClassList(NULL, 0);
  Class *classes;

//! 2
  classes = (Class *)malloc(sizeof(Class) * numberOfClasses);
  objc_getClassList(classes, numberOfClasses);

  NSMutableArray *conformingClasses = [NSMutableArray array];
  for (NSInteger i = 0; i < numberOfClasses; i++) {
    Class lClass = classes[i];
//! 3
    if (class_conformsToProtocol(lClass, protocol)) {
      [conformingClasses addObject:classes[i]];
    }
  }

  free(classes);
  return [conformingClasses copy];
}

What happens here?

  1. First we need to establish the total number of classes in our application, so that we can allocate enough memory to hold them all.
  2. We allocate memory for our classes array and then ask the runtime to fill this allocated memory with the classes.
  3. While we are enumerating over all classes we need to use
class_conformsToProtocol

Instead of NSObject:

[class conformsToProtocol:]

This is because not all classes have to inherit from NSObject or implement the NSObject Protocol and that method will fail.

Next we'd like to be able to grab a class corresponding to a specific serverType value:

it(@"should be able to query class for a serverType", ^() {
  [[[KZBParsingHelper findClassesConformingToProtocol:@protocol(KZBParsableObjectProtocol)] should] contain:KZBTestParsableClass.class];
});

Now querying all the classes every time someone asks for a serverType would be very wasteful and not very smart.

Let's generate a serverType - class mapping only once when the KZBParsingHelper is first used:


//! 1.
+ (void)initialize
{
  [self setupServerTypeToClassMapping];
}

+ (void)setupServerTypeToClassMapping
{
//! 2.
  NSMutableDictionary *classMapping = [NSMutableDictionary new];
  for (Class lClass in [self findClassesConformingToProtocol:@protocol(KZBParsableObjectProtocol)]) {
    classMapping[[(id)lClass serverType]] = lClass;
  }
//! 3.
  NSArray *const serverTypes = classMapping.allValues;
  NSAssert([serverTypes count] == [[serverTypes valueForKeyPath:@"@distinctUnionOfObjects.self"] count], @"serverType collision, there shouldn't be 2 classes using same serverType %@", classMapping);

//! 4.
  objc_setAssociatedObject(self, serverTypeToClassMappingKey, classMapping, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

+ (Class)classForServerType:(NSString *)serverType
{
//! 5.
  return [objc_getAssociatedObject(self, serverTypeToClassMappingKey) objectForKey:serverType];
}

Let's elaborate:

  1. initialize is called when the class is first referenced, so when you first try to use any of the methods available in KZBParsingHelper it will execute.
  2. Create a simple mapping between a serverType and the class that can map it.
  3. Make sure that there aren't any duplicate serverTypes, since that would be programmer mistake.
  4. Use associated objects to store the mappings against the class.
  5. Use associated objects to retrieve a mapping from the class.
Parser

Let's define a simple DataParser protocol:

@protocol KZBParserProtocol <NSObject>
- (void)parseData:(NSData *)data withCompletion:(void (^)(NSError *, NSDictionary *parsingInfo))completionBlock;
@end
  1. I don't see a need to define data types at this level, I'd rather accept raw data and have the specific parser know how to handle it.
  2. Even if you wanted to do the parsing synchronously (really?) I believe this should still be designed as an async interface.
  3. Completion is called with error and parsingInfo, parsingInfo might be useful for specific parsers.
JSON Parser & CoreData

Let's implement a base JSON parser that can work with CoreData, this will be a base class for implementing format specific JSON parsers later on.

@interface KZBJSONParser : NSObject <KZBParserProtocol>
- (void)processBody:(id)jsonBody inContext:(NSManagedObjectContext *)localContext completion:(void (^)(NSError *, NSDictionary *))completionBlock ;
@end

Here we are adding one new method, subclasses can use it to define format specific parsing logic.

The implementation looks like this:


@implementation KZBJSONParser
//! 1.
- (void)parseData:(NSData *)jsonData withCompletion:(void (^)(NSError *, NSDictionary *))completionBlock
{
//! 2.
  NSParameterAssert(completionBlock);

  __block NSError *error;
  id obj = [self deserializeJSONData:jsonData error:&error];
  if (!obj && error) {
    completionBlock(error, nil);
    return;
  }

  __block NSDictionary *parserInfo;
//! 3.
  [MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
    [self processBody:obj
          withCompletion:^(NSError *aError, NSDictionary *aParserInfo) {
            error = aError;
            parserInfo = aParserInfo;
          } inContext:localContext];
  } completion:^(BOOL success, NSError *aError) {
    if (!success && aError) {
      error = pixle_NSErrorMake([NSString stringWithFormat:@"Saving to CoreData failed with error %@", aError], kErrorCodeInternal, aError ? @{@"parseError" : aError} : nil, nil);
    }
    completionBlock(error, parserInfo);
  }];
}

//! 4.
- (void)processBody:(id)jsonBody withCompletion:(void (^)(NSError *, NSDictionary *))completionBlock inContext:(NSManagedObjectContext *)localContext
{
}

#pragma mark - Helpers
//! 5.
- (id)deserializeJSONData:(NSData *)data error:(NSError **)error
{
  if (data == nil) {
    if (error != NULL) {
      NSError *ourError = pixle_NSErrorMake(@"JSON data is nil", kErrorCodeInternal, nil, nil);
      *error = ourError;
    }
    return nil;
  }
  id obj = [NSJSONSerialization JSONObjectWithData:data options:(NSJSONReadingOptions)0 error:error];
  return obj;
}
@end
  1. Concrete implementation of the base method from the Parser protocol.
  2. Parsing is an expensive operation and I'd like to enforce no-one calls it without caring about the results.
  3. Our JSON processing will happen on the CoreData context thread, and we'll pass in the localContext as I prefer to be explicit when working with context objects.
  4. Stub method for subclasses.
  5. Helper method for deserialising JSON using NSJSONSerialization.

Now to be able to progress from here:

We need to know the JSON format coming from our servers.

As nice as REST is, not every client you have to work with have proper REST services, sometimes you might need to handle custom endpoints.

Server API?

While I was lead dev at TAB one of our apps had a diff-like API:

We would query an endpoint and we'd get a response with list of actions to execute on our iOS app.

Let's define something similar for our tests:

{
    "actions": [
        {
            "type": "add",
            "payload": {
                "serverId" : "0",
                "serverType" : "TextWidget",
                "title": "My test title"
            }
        },
        {
            "type": "add",
            "payload": {
                "serverId": "2",
                "serverType": "ImageWidget",
                "url": "http://goo.gl/IFSk4C"
            }
        }],
    "nextUrl" : null
}

We have 2 sections in our response:

  1. actions - this is an array of actions to execute in a specific order. Each action has a type and a payload.
  2. nextUrl - the next fetch request should use this url. This way we can drive the whole API flow and only hardcode the 'start' endpoint in our app.

Let's implement a JSON Diff parser to handle our custom format, with some space for extensibility later on.

JSON Diff parser

The nice thing about our base JSON parser is the fact that we only need to override one method to implement our diff format:

- (void)processBody:(id)jsonBody withCompletion:(void (^)(NSError *, NSDictionary *))completionBlock inContext:(NSManagedObjectContext *)localContext;

Let's define some simple requirements for our diff parser:

  • should raise an exception for JSON format
  • should succeed for proper JSON format
  • should ignore non implemented actions
  • should create a new object for the add action

To satisfy the first 2 requirements we can write tests like this:

//! 1.
  it(@"should raise exception for corrupted JSON format", ^() {
    [[theBlock(^() {
      [sut processBody:[sut deserializeJSONData:DataForDiffFile(invalid) error:nil] withCompletion:^
      (NSError *aError, NSDictionary *dictionary) {
      } inContext:[NSManagedObjectContext MR_context]];
    }) should] raise];
  });

//! 2.
  it(@"should succeed for proper JSON format", ^() {
    __block NSNumber *result = nil;
    [sut parseData:DataForDiffFile(valid) withCompletion:^(NSError *error, NSDictionary *dictionary) {
      result = @(error == nil);
    }];

    [[expectFutureValue(result) shouldEventually] beYes];
  });
  1. This test is not perfect as I had to manually deserialise JSON and use processBody instead of the standard method, this is due to having to raise an exception asynchronously, if you know how to write a test that can work with the default method, send me a tweet.
  2. We want to get a completion callback and make sure there weren't any errors.

We can make these 2 tests pass with a simple implementation like:

- (void)processBody:(id)jsonBody withCompletion:(void (^)(NSError *, NSDictionary *))completionBlock inContext:(NSManagedObjectContext *)localContext
{
  [super processBody:jsonBody withCompletion:completionBlock inContext:localContext];

  AssertTrueOrReturnBlock([jsonBody isKindOfClass:NSDictionary.class], ^(NSError *error) {
    completionBlock(error, nil);
  });

  NSString *nextURL = jsonBody[@"nextUrl"];
  AssertTrueOrReturnBlock((id)nextURL == [NSNull null] || [nextURL isKindOfClass:NSString.class], ^(NSError *error) {
    completionBlock(error, nil);
  });

  NSArray *actions = jsonBody[@"actions"];
  AssertTrueOrReturnBlock([actions isKindOfClass:NSArray.class], ^(NSError *error) {
    completionBlock(error, nil);
  });

  [actions enumerateObjectsUsingBlock:^(NSDictionary *action, NSUInteger idx, BOOL *stop) {
    AssertTrueOrReturnBlock([action isKindOfClass:NSDictionary.class], ^(NSError *error) {
      *stop = YES;
      completionBlock(error, nil);
    });

    AssertTrueOrReturnBlock([action[@"type"] isKindOfClass:NSString.class], ^(NSError *error) {
			*stop = YES;
      completionBlock(error, nil);
    });
  }];
}

Here we assert on expected types using my advanced asserts, that way it won't crash when compiled for release.

But this code already looks a little bit crowded, and not that flexible, so let's refactor it to prepare for the next steps:


- (void)processBody:(id)jsonBody withCompletion:(void (^)(NSError *, NSDictionary *))completionBlock inContext:(NSManagedObjectContext *)localContext
{
  [super processBody:jsonBody withCompletion:completionBlock inContext:localContext];

  AssertTrueOrReturnBlock([jsonBody isKindOfClass:NSDictionary.class], ^(NSError *error) {
    completionBlock(error, nil);
  });
//! 1.
  NSMutableDictionary *parsingInfo = [NSMutableDictionary new];
  NSError *error = [self processSections:jsonBody parsingInfo:parsingInfo inContext:localContext];
  if (error) {
    completionBlock(error, parsingInfo);
    return;
  }

  completionBlock(nil, parsingInfo);
}

- (NSError *)processSections:(NSDictionary *)sections parsingInfo:(NSMutableDictionary *)parsingInfo inContext:(NSManagedObjectContext *)context
{
  __block NSError *oError;
  [sections enumerateKeysAndObjectsUsingBlock:^(NSString *section, id sectionData, BOOL *stop) {
    AssertTrueOrReturnBlock([section isKindOfClass:NSString.class], ^(NSError *error) {
      *stop = YES;
      oError = error;
    });
    //! 2.
    SEL selector = [self selectorForHandlingSection:section];
    if (![self respondsToSelector:selector]) {
      DDLogWarn(@"Ignoring json diff section %@", section);
      return;
    }
    //! 3.
    id (*objc_msgSendTyped)(id, SEL, id, NSMutableDictionary *, NSManagedObjectContext *context) = (void *)objc_msgSend;
    NSError *error = objc_msgSendTyped(self, selector, sectionData, parsingInfo, context);
    if (error) {
      *stop = YES;
      oError = error;
    }
  }];

  return oError;
}

//! 4.
#pragma mark - Parsing sections
- (NSError *)processActions:(id)actionsArray parsingInfo:(NSMutableDictionary *)parsingInfo inContext:(NSManagedObjectContext *)context  __used
{
  __block NSError *oError;

  AssertTrueOrReturnError([actionsArray isKindOfClass:NSArray.class]);
  [actionsArray enumerateObjectsUsingBlock:^(NSDictionary *actionData, NSUInteger idx, BOOL *stop) {
    AssertTrueOrReturnBlock([actionData isKindOfClass:NSDictionary.class], ^(NSError *error) {
      *stop = YES;
      oError = error;
    });

    AssertTrueOrReturnBlock([actionData[@"type"] isKindOfClass:NSString.class], ^(NSError *error) {
      *stop = YES;
      oError = error;
    });

    SEL selector = [self selectorForHandlingAction:actionData[@"type"]];
    if (![self respondsToSelector:selector]) {
      DDLogWarn(@"Ignoring json diff action %@", actionData);
      return;
    }


    id (*objc_msgSendTyped)(id, SEL, NSDictionary *, NSManagedObjectContext *context) = (void *)objc_msgSend;
    NSError *error = objc_msgSendTyped(self, selector, actionData, context);
    if (error) {
      *stop = YES;
      oError = error;
    }
  }];

  return oError;
}

- (NSError *)processNextUrl:(NSString *)nextURL parsingInfo:(NSMutableDictionary *)parsingInfo inContext:(NSManagedObjectContext *)context __used
{
  AssertTrueOrReturnError((id)nextURL == [NSNull null] || [nextURL isKindOfClass:NSString.class]);
  return nil;
}

#pragma mark - Helpers

- (SEL)selectorForHandlingSection:(NSString *)section
{
  return NSSelectorFromString([NSString stringWithFormat:@"process%@:parsingInfo:inContext:", [section MR_capitalizedFirstCharacterString]]);
}

Even though there is more code with extra methods this is easier to read and future-proofed:

  1. Parsing all sections is now done in a separate method.
  2. We now have dynamic resolution of section handling, which means we don't hardcode supported sections, this can be very useful in the future of the project, when you need to add support for new sections, I'll show an example later on.
  3. We use a typed objc_msgSend to call the selector, this is due to the fact that we need to be careful about types when working with ARC and arm64.
  4. The previous code has been refactored into separate methods that are called dynamically, don't forget to use __used keyword to prevent compiler from thinking this code is dead.

We have 2 remaining requirements:

  1. should ignore non implemented actions
  2. should create a new object for the add action

The 1st requirement is tested with same code as the success requirement, only using different JSON file.

The 2nd one is more complicated:

  • we need some test classes that use our CoreData model.
  • we need to make sure our CoreData is set-up correctly for testing.
  • we need to finish our processActions section handling.

Let's start with a very basic data model:

{{< photo src="/2014/04/Model.png" width="344" height="219" >}}

To be able to write tests with CoreData I'd recommend using beforeEach/afterEach blocks in Kiwi, this is how they look after adding CD memory store:

     __block KZBJSONDiffParser *sut;
      beforeEach(^{
        [MagicalRecord setupCoreDataStackWithInMemoryStore];

        sut = [KZBJSONDiffParser new];
      });

      afterEach(^{
        [MagicalRecord cleanUp];
      });

      it(@"should ignore non implemented actions", ^() {
        __block NSNumber *result = nil;
        [sut parseData:DataForDiffFile(newactions) withCompletion:^(NSError *error, NSDictionary *dictionary) {
          result = (error == nil ? @YES : @NO);
        }];

        [[expectFutureValue(result) shouldEventually] beYes];
      });

      it(@"should create a new object for the add action", ^() {
        __block id object = nil;
        [sut parseData:DataForDiffFile(valid) withCompletion:^(NSError *error, NSDictionary *dictionary) {
          object = [KZBTextWidget MR_findFirst];
        }];

        [[expectFutureValue(object) shouldEventually] beNonNil];
      });

Now we need to expand our processActions method to actually do something constructive.

Since we added a nice flexible way for dealing with section sections, let's replicate that for actions as well:

    SEL selector = [self selectorForHandlingAction:actionData[@"type"]];
    if (![self respondsToSelector:selector]) {
      DDLogWarn(@"Ignoring json diff action %@", actionData);
      return;
    }

    id (*objc_msgSendTyped)(id, SEL, id) = (void *)objc_msgSend;
    NSError *error = objc_msgSendTyped(self, selector, actionData);
    if (error) {
      *stop = YES;
      oError = error;
    }

For the actionAdd itself, you can implement it like this:

- (NSError *)actionAdd:(NSDictionary *)action inContext:(NSManagedObjectContext *)context __used
{
  action = action[@"payload"];
  AssertTrueOrReturnNil([action isKindOfClass:NSDictionary.class]);

  NSString *serverType = action[@"serverType"];
  AssertTrueOrReturnNil([serverType isKindOfClass:NSString.class]);

  NSString *serverID = action[@"serverId"];
  AssertTrueOrReturnNil([serverID isKindOfClass:NSString.class]);

  NSManagedObject <KZBParsableObjectProtocol> *obj = [KZBParsingHelper findOrCreateWithServerType:serverType serverID:serverID inContext:context];
  [obj updateFromDictionary:action];

  return nil;
}
  • We are only interested in payload at this point.
  • Asserts to make sure our format is matching expectations
  • Only soft-errors, if we return an Error from here it will stop the whole parsing, instead I'd like to ignore incorrect data in this particular case, so I am using Asserts but not returning errors (my asserts already generate and log error info).
  • findOrCreate object and ask it to update itself from the action dictionary.

Keep in mind that in normal projects:

  • I'd recommend having a separate helper for CoreData stuff.
  • findOrCreate should be optimised, doing it per action is too slow when you have lots of them. It's quite simple to optimise it but beyond scope of this article.

This is how findOrCreate is implemented in ParsingHelper:

+ (NSManagedObject <KZBParsableObjectProtocol> *)findOrCreateWithServerType:(NSString *)serverType serverID:(NSString *)serverID inContext:(NSManagedObjectContext *)context
{
  Class oClass = nil;
  NSManagedObject <KZBParsableObjectProtocol> *obj = [self findWithServerType:serverType serverID:serverID inContext:context classForObject:&oClass];
  if (!obj && oClass) {
    obj = [oClass MR_createInContext:context];
    [obj setValue:serverID forKey:[(id)oClass serverIDPropertyName]];
  }
  return obj;
}

+ (NSManagedObject <KZBParsableObjectProtocol> *)findWithServerType:(NSString *)serverType serverID:(NSString *)serverID inContext:(NSManagedObjectContext *)context classForObject:(out Class *)oClass
{
  Class classForObject = [KZBParsingHelper classForServerType:serverType];
  if (oClass) {
    *oClass = classForObject;
  }
  if (!classForObject) {
    DDLogInfo(@"Ignoring object with serverType %@ as there is no matching class", serverType);
    return nil;
  }

  AssertTrueOrReturnNilBlock([self checkIfClass:classForObject isKindOfClass:NSManagedObject.class], ^(NSError *error) {
    if (oClass) {
      *oClass = nil;
    }
  });
  NSString *const serverIDProperty = [(id)classForObject serverIDPropertyName];
  NSManagedObject <KZBParsableObjectProtocol> *obj = [classForObject MR_findFirstByAttribute:serverIDProperty withValue:serverID inContext:context];
  return obj;
}
  • We ignore serverTypes that don't have a matching class
  • We assert this parsable object is a managed object since this is a CoreData method.
  • We try finding an existing object. If one doesn't exist but we have found a proper class we use this class to create a new instance.

Now all tests pass. Our architecture is finally ready to parse some data.

Parsing object data

Let's add parsing to our ImageWidget, since we spennt some time creating a nice architecture, it's going to be very straightforward.

First let's see if we can get our URL parsed correctly and transformed into an NSURL.

it(@"should create ImageWidget with properly parsed data", ^() {
  __block KZBImageWidget *imageWidget = nil;
  [sut parseData:DataForDiffFile(valid) withCompletion:^(NSError *error, NSDictionary *dictionary) {
    imageWidget = [KZBImageWidget MR_findFirst];
  }];

  [[expectFutureValue(imageWidget.url) shouldEventually] equal:[NSURL URLWithString:@"http://goo.gl/IFSk4C"]];
});

So how hard is it to add support for parsing image widgets with this architecture?

//! in .h
@interface KZBImageWidget : _KZBImageWidget <KZBParsableObjectProtocol>{}

//! in .m
@implementation KZBImageWidget

+ (NSString *)serverType
{
  return @"ImageWidget";
}

- (void)updateFromDictionary:(NSDictionary *)dictionary
{
  [KZPropertyMapper mapValuesFrom:dictionary toInstance:self usingMapping:@{
    @"url" : KZBox(URL, url),
    @"caption" : KZProperty(caption)
  }];
}
@end

And that's it. Pretty simple isn't it? You just conform to the protocol, specify the class and describe the mapping.

Yeah right

That was really simple data, what if I'd like to have a CoreData relationship? It's probably going to be dreadful and hard?

Not really.

Let's say our server architecture changes and we need to add a relationship between ImageWidget and TextWidget:

 {
            "type": "add",
            "payload": {
                "serverId": "2",
                "serverType": "ImageWidget",
                "url": "http://goo.gl/IFSk4C",
                "caption" : "it works!",
				"textWidgetId" : "0"
            }
        }

How can we change our parsing to handle this? 2 Options:

  1. Add boxing in a category to be able to create objects like that, preferable if you need to repeat this kind of mapping in multiple places.
  2. Just use KZCall to create sub-object:

First let's add test for our new requirement:

it(@"should create ImageWidget with properly parsed relationship", ^() {
  __block BOOL isEqual = NO;
  [sut parseData:DataForDiffFile(valid) withCompletion:^(NSError *error, NSDictionary *dictionary) {
    KZBImageWidget *imageWidget = [KZBImageWidget MR_findFirst];
    KZBTextWidget *textWidget = [KZBTextWidget MR_findFirst];
    isEqual = imageWidget && imageWidget.textWidget == textWidget;
  }];

  [[expectFutureValue(@(isEqual)) shouldEventually] beTrue];
});

Now we need to update our KZBImageWidget parsing like this:

- (void)updateFromDictionary:(NSDictionary *)dictionary
{
  [KZPropertyMapper mapValuesFrom:dictionary toInstance:self usingMapping:@{
    @"url" : KZBox(URL, url),
    @"caption" : KZProperty(caption),
    @"textWidgetId" : KZCall(objectForId:, textWidget)
  }];
}

- (id)objectForId:(NSString *)serverID
{
  AssertTrueOrReturnNil([serverID isKindOfClass:NSString.class]);
  return [KZBParsingHelper findWithServerType:[KZBWidget serverType] serverID:serverID inContext:self.managedObjectContext];
}

Looks simple enough?

  1. Use KZCall to specify the selector to be called with textWidgetId value, the resulting value should be assigned to the textWidget property.
  2. objectForId method that will query ANY subclass of KZBWidget that has specific serverID.

I've implemented objectForId without textWidgetType because you could move this code to KZPropertyMapper as a new boxing and have it used in ALL your relationships if needed.

KZPM offers more than we used here:

  • It does compile time checking for your property / method names
  • Allows extra simple expansion by using categories
  • Offers simple validation logic with a custom DSL syntax

Read more at it's github page.

Conclusion

By now you should know how to create simple, yet flexible architecture.
I also hope you noticed that by writing tests, you can find your mistakes and verify assumptions with ease.

Keep in mind

  • In normal projects you should definitely have more tests and they could be refactored a little bit.
  • This is the general idea, but it's a good start, I'd adapt it to project specific needs.

Grab source code from GitHub

You've successfully subscribed to Krzysztof Zabłocki
Great! Next, complete checkout to get full access to all premium content.
Error! Could not sign up. invalid link.
Welcome back! You've successfully signed in.
Error! Could not sign in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.