CocoaPods trunk is moving to be read-only. Read more on the blog, there are 18 months to go.
TestsTested | ✓ |
LangLanguage | Obj-CObjective C |
License | MIT |
ReleasedLast Release | Dec 2014 |
Maintained by Zach Radke.
on master
on develop
A lightweight immutable model framework disguised by friendly food metaphors.
To use ZCREasyBake, your project should have a minimum deployment target of iOS 5.1+ or OSX 10.7+ and be running with ARC. However, this project is only unit tested on iOS 7.0+ and OSX 10.9+.
ZCREasyBake can be installed a variety of ways depending on your preference:
Classes
folder into your project.pod "ZCREasyBake"
to your Podfile for Cocoapods.However you get the framework into your project, you can import the main header where needed:
#import <ZCREasyBake/ZCREasyBake.h> // Or #import "ZCREasyBake.h"
Throughout the ZCREasyBake project, you'll encounter some jargon:
readonly
properties when they expose a model's underlying application data.NSObject
and NSCopying
protocols that can uniquely identify a model across instances.NSDictionary
or NSArray
. This data is typically produced by an external service, such as a web API, and can be processed into models after some work.ZCREasyRecipe
. Recipes let us decouple ingredient sources from their final model representations. Recipes are usually model and ingredient-source dependent, but otherwise reusable.Start by defining your model as a subclass of ZCREasyDough
:
@interface User : ZCREasyDough
@property (strong, readonly) NSString *name;
@property (assign, readonly) NSUInteger unreadMessages;
@property (strong, readonly) NSDate *updatedAt;
@end
Note that the properties are defined as readonly
so the instance is effectively immutable after creation!
Your models will need to be populated with ingredients from an ingredient source. For example, our User
model defined above can be backed by a web service that returns JSON like so:
{
"server_id": "1209-3r47-4482-9rj4-93iu-324s",
"name": "Zach Radke",
"unread_messages": 10,
"updated_at": "2014-04-19T19:32:05Z"
}
Whatever the ingredient source, the ingredients must be either an NSDictionary
or NSArray
before they can be processed.
To process raw ingredients into a model, a ZCREasyRecipe
is used. These recipes are usually model and ingredient-source dependent. For example, we would create a single recipe for the User
model and JSON ingredient source defined above.
All recipes begin with an NSDictionary
mapping, which is required. The keys are property keys of the model to populate, and the values are the corresponding ingredient paths from the ingredient-source.
NSDictionary *mapping = @{@"name": @"name",
@"unreadMessages": @"unread_messages",
@"updatedAt": @"updated_at"};
The ingredient path is represented as a string, but may use dot notation to indicate a dictionary-key traversal, or the form [<index>]
to indicate an array-index traversal. For example:
// The mapping…
NSDictionary *mapping = @{@"key": @"values[0].key"};
// Which corresponds to this structure…
NSDictionary *ingredientSource = @{@"values": @[@{@"key": @"fooBar"}]};
// Would produce these processed ingredients…
NSDictionary *processedIngredients = @{@"key": @"fooBar"};
Recipes may also optionally provide a dictionary of transformers to use for processing the raw ingredients into different objects. As with the ingredient mapping, the keys are property keys on the model which should be transformed. The values are NSValueTransformer
instances or NSStrings
. If strings are used, they must be registered to value transformers.
// NSValueTransformer+DefaultTransformers.h
// Assume we have created the DateTransformer class elsewhere...
DateTransformer *dateTransformer = [[DateTransformer alloc] initWithDateFormat:@"yyyy-MM-dd'T'HH-mm-ss'Z'"];
[NSValueTransformer setValueTransformer:dateTransformer forName:@"DateTransformer"];
// ...
NSDictionary *transformers = @{@"updatedAt": @"DateTransformer"};
Finally, a recipe may have a name. This is useful for debugging purposes, but also for storing and reusing recipes in ZCREasyRecipeBox
instances.
ZCREasyRecipe *userJSONRecipe = [ZCREasyRecipe makeWith:^(id<ZCREasyRecipeMaker maker) {
[maker setIngredientMapping:mapping];
[maker setIngredientTransformers:transformers];
[maker setName:@"UserJSONRecipe"];
}];
[[ZCREasyRecipeBox defaultBox] addRecipe:userJSONRecipe error:NULL];
Since recipes are typically model dependent, you can also provide class methods on the model for even easier recipe access.
// User.m
+ (ZCREasyRecipe *)JSONRecipe {
return [[ZCREasyRecipeBox defaultBox] recipeWithName:@"UserJSONRecipe"];
}
ZCREasyRecipe
and ZCREasyRecipeBox
have many utilities that make generating and validating recipes much easier. Check their headers for more information.
Your model will inherit the designated initializers from ZCREasyDough
:
- (instancetype)initWithIdentifier:(id<NSObject,NSCopying>)identifier
ingredients:(id)ingredients
recipe:(ZCREasyRecipe *)recipe
error:(NSError **)error;
+ (instancetype)makeWith:(void (^)(id<ZCREasyBaker> baker))constructionBlock;
To create an instance you'll need an identifier, some ingredients, and a recipe:
NSDictionary *ingredients = @{@"server_id": @"1209-3r47-4482-9rj4-93iu-324s"
@"name": @"Zach Radke"
@"unread_messages": @10,
@"updated_at": @"2014-04-19T19:32:05Z"};
User *user = [User prepareWith:^(id<ZCREasyChef> chef) {
[chef setIdentifier:ingredients[@"server_id"]];
[chef setIngredients:ingredients];
[chef setRecipe:[User JSONRecipe]];
}];
When you want to update an instance, simply use the update method on it, passing the new ingredients and the recipe to generate a new instance:
NSDictionary *ingredients = @{@"name": @"Zachary Radke"};
User *updatedUser = [user updateWithIngredients:ingredients
recipe:[User JSONRecipe]
error:NULL];
The updated instance will share the same unique identifier as it's parent, and will generate notifications that can be observed:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(userUpdated:)
name:[User updateNotificationName]
object:nil];
For a more generic notification, the ZCREasyDoughUpdatedNotification
can be observed, which will be triggered for updates to all ZCREasyDough
subclasses.
Equality between ZCREasyDough
subclasses is determined by the identifier used when initializing an instance. This means that calls to isEqual:
will return YES
between a model and an updated model:
[user isEqual:updatedUser]; // YES
(user == updatedUser); // NO
A subclass can also report whether it already contains given ingredients with a given recipe:
NSDictionary *ingredients = @{@"name": @"Zachary Radke"};
[user isEqualToIngredients:ingredients withRecipe:[User JSONRecipe] error:NULL]; // NO
[updatedUser isEqualToIngredients:ingredients withRecipe:[User JSONRecipe] error:NULL]; // YES
From ingredients it was made and to ingredients it shall return! A model can be decomposed using a given recipe:
NSDictionary *ingredients = [updatedUser decomposeWithRecipe:[User JSONRecipe] error:NULL];
The resulting type will either be an NSDictionary
or an NSArray
depending on what the recipe's mapping suggests the root object is.
If the recipe supplies value transformers, they will be applied to the model's value if the transformer supports reverse transformations.
[DateTransformer allowsReverseTransformation]; // YES
ingredients[@"updated_at"]; // @"2014-04-19T19:32:05Z"
Running into difficulties with your models? Maybe these tips can help:
setValue:forKey:
.// Invalid mapping since key1 indicates a dictionary and key2 indicates an array
NSDictionary *invalidMapping = @{@"key1": @"key_1",
@"key2": @"[0]"};
NSNull
values are converted to nil
for transformers.nil
it will be converted to NSNull
in the processed ingredients.NSNull
ingredient values are converted to nil
.readonly
properties cannot be set via setValue:forKey:
. Attempts to do so will raise a ZCREasyDoughExceptionAlreadyBaked
exception.updateWithIngredients:recipe:error
is called with ingredients that are already part of the model, no notifications will be posted, and the same object will be returned rather than a new instance.ZCREasyDough
class introspects your model's properties at runtime and caches them, so avoid dynamically creating properties on your model class at runtime.