Skip to content

Seamless Web Service Integration and Core Data Model Population

License

Notifications You must be signed in to change notification settings

jt26077/MMRecord

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

57 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MMRecord Blog Banner

MMRecord is a block-based seamless web service integration library for iOS and Mac OS X. It leverages the Core Data model configuration to automatically create and populate a complete object graph from an API response. It works with any networking library, is simple to setup, and includes many popular features that make working with web services even easier. Here's how to make a request for App.net Post records:

NSManagedObjectContext *context = [[MMDataManager sharedDataManager] managedObjectContext];

[Post 
 startPagedRequestWithURN:@"stream/0/posts/stream/global"
 data:nil
 context:context
 domain:self
 resultBlock:^(NSArray *posts, ADNPageManager *pageManager, BOOL *requestNextPage) {
	 NSLog(@"Posts: %@", posts);
 }
 failureBlock:^(NSError *error) {
	 NSLog(@"%@", error);
 }];

Keep reading to learn more about how to start using MMRecord in your project!

Getting Started

  • Download MMRecord and try out the included example apps.
  • Continue reading the integration instructions below.
  • Check out the documentation for all the rest of the details.

Overview

MMRecord is designed to make it as easy and fast as possible to obtain native objects from a new web service request. It handles all of the fetching, creation, and population of NSManagedObjects for you in the background so that when you make a request, all you get back is the native objects that you can use immediately. No parsing required.

The library is architected to be as simple and lightweight as possible. Here's a breakdown of the core classes in MMRecord.

Core
MMRecord A subclass of NSManagedObject that defines the MMRecord interface and initiates the object graph population process.
  <ul>
    <li>Entry point for making requests</li>
    <li>Uses a registered <tt>MMServer</tt> class for making requests</li>
    <li>Initiaties the population process using the <tt>MMRecordResponse</tt> class</li>
	<li>Returns objects via a block based interface</li>
  </ul>
</td>
MMServer An abstract class that defines the request interface used by MMRecord.
  <ul>
    <li>Designed to be subclassed</li>
    <li>Supports any networking framework, including local files and servers</li>
  </ul>
</td>

MMRecord Architecture Diagram

Population
MMRecordResponse A class that handles the process of turning a response into native MMRecord objects.
MMRecordProtoRecord A container class used as a placeholder for the object graph during the population process.
MMRecordRepresentation A class that defines the mapping between a dictionary and a Core Data NSEntityDescription.
MMRecordMarshaler A class responsible for populating an instance of MMRecord based on the MMRecordRepresentation.
Pagination
MMServerPageManager An abstract class that defines the interface for handling pagination.
Caching
MMRecordCache An class that maps NSManagedObject ObjectIDs to an NSCachedURLResponse .

MMRecord Population Architecture

Integration Guide

MMRecord does require some basic setup before you can use it to make requests. This guide will go take you through the steps in that configuration process.

Server Class Configuration

MMRecord requires a registered server class to make requests. The server class should know how to make a request to the API you are integrating with. The only requirement of a server implementation is that it return a response object (array or dictionary) that contains the objects you are requesting. A server might use AFNetworking to perform a GET request to a specific API. Or it might load and return local JSON files. There are two sub specs which provide pre-built servers that use AFNetworking and local JSON files. Generally speaking though, you are encouraged to implement your own server to talk to the API you are using.

Once you have defined your server class, you must register it with MMRecord:

[Post registerServerClass:[ADNServer class]];

Note that you can register different server classes on different subclasses of MMRecord.

[Tweet registerServerClass:[TWSocialServer class]];
[User registerServerClass:[MMJSONServer class]];

This is helpful if one endpoint you are working with is complete, but another is not, or is located on another API.

MMRecord Subclass Implementation

You are required to override one method on your subclass of MMRecord in order to tell the parsing system where to locate the object(s) are tbat you wish to parse. This method returns a key path that specifies the location relative to the root of the response object. If your response object is an array, you can just return nil.

In an App.net request, all returned objects are located in an object called "data", so our subclass of MMRecord will look like this:

@interface ADNRecord : MMRecord
@end

static NSDateFormatter *ADNRecordDateFormatter;

@implementation ADNRecord

+ (NSString *)keyPathForResponseObject {
    return @"data";
}

+ (NSDateFormatter *)dateFormatter {
    if (!ADNRecordDateFormatter) {
        NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZ"]; // "2012-11-21T03:57:39Z"
        ADNRecordDateFormatter = dateFormatter;
    }
    
    return ADNRecordDateFormatter;
}

@end

There are also some optional methods you may wish to implement on MMRecord. One such method returns a date formatter configured for populating attributes of type Date. You can override this method to populate date attributes using a formatted date string. Unix number time stamp dates are supported by default.

Note that these methods were implemented on a class called ADNRecord, which is a subclass of MMRecord. Additional entities are subclasses of ADNRecord, and do not need to implement these methods themselves.

Model Configuration

The Core Data NSManagedObjectModel is very useful. At a highlevel, the model is composed of a list of entities. Likewise, an API is typically composed of a list of endpoints. MMRecord takes advantage of this convention to map an entity representation to an endpoint representation in order to create native objects from an API response.

MMRecord leverages the introspective properties of the Core Data model to decide how to parse a response. The class you start the request from is considered to be the root of the object graph. From there, MMRecord looks at that NSEntityDescription's attributes and relationships and attempts to populate each of them from the given response object. That information is very helpful, because it makes population of most attributes very straightforward. Because of this, it's helpful if your data model entity representations maps very closely to your API endpoint response representations.

Primary Key

MMRecord works best if there is a way to uniquely identify records of a given entity type. That allows it to fetch the existing record (if it exists) and update it, rather than create a duplicate one. To designate the primary key for an entity, we leverage the entity's user info dictionary. Specify the name of the primary key property as the value, and MMRecordEntityPrimaryAttributeKey as the key.

MMRecord Primary Key

Note that the primary key can be any property, which includes a relationship. If a relationship is used as the primary key, MMRecord will attempt to fetch the parent object and search for the associated object in the relationship.

MMRecord Relationship Primary Key

Alternate Property Names

Sometimes, you may need to define an alternate name for a property on one of your entities. This could be for a variety of reasons. Perhaps you don't like your Core Data property names to include underscores? Perhaps the API response changed, and you don't want to change your NSManagedObject property names. Or maybe the value of a property is actually inside of a sub-object, and you need to bring it up to root level. Well, that's what the MMRecordAttributeAlternateNameKey is for. You can define this key on any attribute or relationship user info dictionary. The value of this key can be an alternate name, or alternate keyPath that will be used to locate the object for that property.

MMRecord Alternate Name Key

For reference, here's a truncated version of the App.net User object to illustrate how those configuration values were determined:

{
    "id": "1", // note this is a string
    "username": "johnappleseed",
    "name": "John Appleseed",
    "avatar_image": {
        "height": 512,
        "width": 512,
        "url": "https://example.com/avatar_image.jpg",
        "is_default": false
    },
    "cover_image": {
        "width": 320,
        "height": 118,
        "url": "https://example.com/cover_image.jpg",
        "is_default": false
    },
	"counts": {
		"following": 100,
		"followers": 200,
        "posts": 24,
        "stars": 76
    }
}

Example Usage

Standard Request

+ (void)favoriteTweetsWithContext:(NSManagedObjectContext *)context
                           domain:(id)domain
                      resultBlock:(void (^)(NSArray *tweets))resultBlock
                     failureBlock:(void (^)(NSError *))failureBlock {
    [Tweet startRequestWithURN:@"favorites/list.json"
                          data:nil
                       context:context
                        domain:self
                   resultBlock:resultBlock
                  failureBlock:failureBlock];
}

Paginated Request

@interface Post : ADNRecord
+ (void)getStreamPostsWithContext:(NSManagedObjectContext *)context
                           domain:(id)domain
                      resultBlock:(void (^)(NSArray *posts, ADNPageManager *pageManager, BOOL *requestNextPage))resultBlock
                     failureBlock:(void (^)(NSError *error))failureBlock;
@end

@implementation Post
+ (void)getStreamPostsWithContext:(NSManagedObjectContext *)context
                           domain:(id)domain
                      resultBlock:(void (^)(NSArray *posts, ADNPageManager *pageManager, BOOL *requestNextPage))resultBlock
                     failureBlock:(void (^)(NSError *error))failureBlock {
    [self startPagedRequestWithURN:@"stream/0/posts/stream/global"
                              data:nil
                           context:context
                            domain:self
                       resultBlock:resultBlock
                      failureBlock:failureBlock];
}
@end

Batched Request

[Tweet startBatchedRequestsInExecutionBlock:^{
    [Tweet
     timelineTweetsWithContext:context
     domain:self
     resultBlock:^(NSArray *tweets, MMServerPageManager *pageManager, BOOL *requestNextPage) {
         NSLog(@"Timeline Request Complete");
     }
     failureBlock:^(NSError *error) {
         NSLog(@"%@", error);
     }];
    
    [Tweet
     favoriteTweetsWithContext:context
     domain:self
     resultBlock:^(NSArray *tweets, MMServerPageManager *pageManager, BOOL *requestNextPage) {
         NSLog(@"Favorites Request Complete");
     }
     failureBlock:^(NSError *error) {
         NSLog(@"%@", error);
     }];
} withCompletionBlock:^{
    NSLog(@"Request Complete");
}];

Fetch First Request

NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"User"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF.name contains[c] %@", name];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES];
fetchRequest.predicate = predicate;
fetchRequest.sortDescriptors = @[sortDescriptor];

[self
 startRequestWithURN:[NSString stringWithFormat:@"stream/0/users/%@", name]
 data:nil
 context:context
 domain:domain
 fetchRequest:fetchRequest
 customResponseBlock:nil resultBlock:^(NSArray *records, id customResponseObject, BOOL requestComplete) {
     if (resultBlock != nil) {
         resultBlock(records, requestComplete);
     }
 }
 failureBlock:failureBlock];

Requirements

MMRecord 1.0 and higher requires either iOS 5.0 and above, or Mac OS 10.7 (64-bit with modern Cocoa runtime) and above.

ARC

MMRecord uses ARC.

If you are using MMRecord in your non-arc project, you will need to set a -fobjc-arc compiler flag on all of the MMRecord source files.

Credits

MMRecord was created by Conrad Stoll at Mutual Mobile.

License

MMRecord is available under the MIT license. See the LICENSE file for more info.

About

Seamless Web Service Integration and Core Data Model Population

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Objective-C 95.9%
  • C 3.9%
  • Other 0.2%