Skip to content

Instantly share code, notes, and snippets.

@samuraisam
Created December 6, 2013 19:46
Show Gist options
  • Select an option

  • Save samuraisam/7831026 to your computer and use it in GitHub Desktop.

Select an option

Save samuraisam/7831026 to your computer and use it in GitHub Desktop.
//
// ManagedObject.h
//
// Created by Samuel Sutch on 10/21/13.
// Copyright (c) 2013 Steamboat Labs. All rights reserved.
//
#import <CoreData/CoreData.h>
#import <AFNetworking/AFNetworking.h>
#import <JSONKit.h>
#import "HTTPClient.h"
@protocol IMPManagedObject <NSObject>
+ (NSString *)singleObjectRequestPath;
+ (NSArray *)identificationAttributes;
+ (NSString *)objectListRequestPath;
- (void)setUrl:(NSString *)url;
@end
FOUNDATION_EXPORT NSString *const IMPPathParametersKey;
FOUNDATION_EXPORT NSString *const IMPRequstGETParameters;
FOUNDATION_EXPORT NSString *const IMPClearCollectionBeforeSaving;
@interface NSMutableURLRequest (ManagedObjectAdditions)
@property (nonatomic) NSDictionary *options;
@end
@interface ManagedObject : NSManagedObject <IMPManagedObject>
/*!
* @brief Set up the core data stack. Returns NSError on fatal errors (nil on success or otherwise
* non-fatal errors)
*/
+ (NSError *)initializeCoreDataStack;
/*!
* @brief Convenience method to get the main queue managed object context. All reads should be performed in
* this context
*/
+ (NSManagedObjectContext *)mainQueueManagedObjectContext;
/*!
* @brief Get the persistent managed object context. All persistent updates should be made in this context.
*/
+ (NSManagedObjectContext *)persistentStoreManagedObjectContext;
/*!
* @brief Convenience method that assumes your entity is named the same as your class
*/
+ (NSString *)entityName;
/*!
* @brief Convenience method to return a fetch request for the current entity
*/
+ (NSFetchRequest *)fetchRequest;
/*!
* @brief Convenience method to query for objects matching a given predicate format
* @discussion Builds a NSFetchRequest using an NSPredicate from the predicateFormat provided.
* Uses the mainQueueManagedObjectContext
* @returns An array of the entities, or nil.
*/
+ (NSArray *)objectsMatchingInContext:(NSManagedObjectContext *)managedObjectContext
format:(NSString *)predicateFormat, ...;
/*!
*/
+ (void)deleteAllObjectsAndWait;
/*!
*/
+ (NSMutableURLRequest *)getRequestPath:(NSString *)path method:(NSString *)method options:(NSDictionary *)options;
/*!
*/
+ (NSMutableURLRequest *)postRequestPath:(NSString *)path method:(NSString *)method options:(NSDictionary *)options;
/*!
*/
+ (void)doRequest:(NSMutableURLRequest *)request
success:(HTTPRequestSuccessBlock)success
failure:(HTTPRequestErrorBlock)failure;
/*!
* @brief Update properties with a JSON resource from the network. By default just updates self.url
* @discussion An NSManagedObjectContext must be provided because implementations of this method (in subclasses) may
* need to call fromURL or fromJSON to resolve/fill out relations.
*
* Implementations SHOULD NOT call -save: on the managedObjectContext
*/
- (void)updateWithJSON:(NSDictionary *)data managedObjectContext:(NSManagedObjectContext *)managedObjectContext;
/*!
* @brief Finds an instance of this type in the provided NSManagedObjectContext. nil if not found.
*/
+ (instancetype)fromURL:(NSString *)url managedObjectContext:(NSManagedObjectContext *)managedObjectContext;
/*!
* @brief Finds/updates OR creates an instance of this type in the NSManagedObjectContext.
*/
+ (instancetype)fromJSON:(NSDictionary *)data managedObjectContext:(NSManagedObjectContext *)managedObjectContext;
@end
//
// ManagedObject.m
//
// Created by Samuel Sutch on 10/21/13.
// Copyright (c) 2013 Steamboat Labs. All rights reserved.
//
#import "ManagedObject.h"
#import "NSString+NSArrayFormatExtensions.h"
#import <AFOAuth2Client/AFOAuth2Client.h>
#import <objc/runtime.h>
#import "HTTPClient.h"
NSString *const IMPPathParametersKey = @"pathParams";
NSString *const IMPRequstGETParameters = @"getParams";
NSString *const IMPClearCollectionBeforeSaving = @"clearCache";
@implementation NSMutableURLRequest (ManagedObjectAdditions)
- (void)setOptions:(NSDictionary *)options
{
objc_setAssociatedObject(self, "REQUEST_OPTIONS", options, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSDictionary *)options
{
return objc_getAssociatedObject(self, "REQUEST_OPTIONS");
}
@end
/*
* Since we have two managed object contexts (one to preform our updates on, one to do reads from) changes made to the
* "background" context need to be merged into the foreground context. This class facilitates that
*/
@interface ManagedObjectContextChangeMergingObserver : NSObject
@property (nonatomic, weak) NSManagedObjectContext *observedContext;
@property (nonatomic, weak) NSManagedObjectContext *mergeContext;
@property (nonatomic, strong) NSSet *objectIDsFromChildDidSaveNotification;
- (id)initWithObservedContext:(NSManagedObjectContext *)observedContext
mergeContext:(NSManagedObjectContext *)mergeContext;
@end
@implementation ManagedObjectContextChangeMergingObserver
- (id)initWithObservedContext:(NSManagedObjectContext *)observedContext
mergeContext:(NSManagedObjectContext *)mergeContext
{
self = [super init];
if (self) {
self.observedContext = observedContext;
self.mergeContext = mergeContext;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleManagedObjectContextDidSaveNotification:)
name:NSManagedObjectContextDidSaveNotification
object:observedContext];
NSManagedObjectContext *context = [mergeContext parentContext];
while (context) {
if ([context isEqual:observedContext]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleManagedObjectContextWillSaveNotification:)
name:NSManagedObjectContextDidSaveNotification
object:mergeContext];
break;
}
context = [context parentContext];
}
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (NSSet *)getObjIdsFromNotification:(NSNotification *)notification
{
NSUInteger count = [[[notification.userInfo allValues] valueForKeyPath:@"@sum.@count"] unsignedIntegerValue];
NSMutableSet *objectIDs = [NSMutableSet setWithCapacity:count];
for (NSSet *objects in [notification.userInfo allValues]) {
[objectIDs unionSet:[objects valueForKey:@"objectID"]];
}
return objectIDs;
}
- (void)handleManagedObjectContextWillSaveNotification:(NSNotification *)notification
{
self.objectIDsFromChildDidSaveNotification = [self getObjIdsFromNotification:notification];
}
- (void)handleManagedObjectContextDidSaveNotification:(NSNotification *)notification
{
NSAssert([notification object] == self.observedContext,
@"Received Managed Object Context Did Save Notification for Unexpected Context: %@",
[notification object]);
if (![self.objectIDsFromChildDidSaveNotification isEqual:[self getObjIdsFromNotification:notification]]) {
[self.mergeContext performBlock:^{
[self.mergeContext mergeChangesFromContextDidSaveNotification:notification];
}];
} else {
NSLog(@"Skipping merge of `NSManagedObjectContextDidSaveNotification`: the save event originated from the "
"mergeContext and thus no save is necessary.");
}
self.objectIDsFromChildDidSaveNotification = nil;
}
@end
@implementation ManagedObject
#pragma mark -
#pragma mark IMPManagedObject implementation
+ (NSString *)singleObjectRequestPath
{
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"You need to implement +singleObjectRequestPath in your subclass"
userInfo:nil];
}
+ (NSArray *)identificationAttributes
{
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"You need to implement +identificationAttributes in your subclass"
userInfo:nil];
}
+ (NSString *)objectListRequestPath
{
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"You need to implement +objectListRequestPath in your subclass"
userInfo:nil];
}
#pragma mark - coredata stuff
//----------------------------------------------------------------------------------------------------------------------
static NSString *PersistentStoreManagedObjectContextKey = @"psmock";
static NSString *MainQueueManagedObjectKey = @"mqmock";
static NSString *ManagedObjectContextChangeMergingObserverKey = @"moccmok"; // omgwtfbbqsauce
static BOOL initialized = NO;
+ (NSError *)initializeCoreDataStack
{
if (initialized) {
return [NSError errorWithDomain:@"Error" code:420 userInfo:@{NSLocalizedDescriptionKey: @"already did it"}];
}
if ([ManagedObject class] != self) {
return [NSError errorWithDomain:@"Error" code:420 userInfo:@{NSLocalizedDescriptionKey:
@"Must be called on ManaegdObject explicitly"}];
}
// setup the persistent store --------------------------------------------------------------------------------------
NSManagedObjectModel *managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:nil];
NSPersistentStoreCoordinator *persistentStoreCoordinator
= [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel];
NSArray *dirs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
if (!dirs.count) {
return [NSError errorWithDomain:@"Error" code:420 userInfo:@{NSLocalizedDescriptionKey: @"no docs directory"}];
}
NSString *dbPath = [dirs[0] stringByAppendingPathComponent:@"objects.sqlite"];
NSURL *storeURL = [NSURL fileURLWithPath:dbPath];
NSDictionary *storeOptions = @{NSInferMappingModelAutomaticallyOption: @YES,
NSMigratePersistentStoresAutomaticallyOption: @YES};
NSError *addStoreError = nil;
NSPersistentStore *persistentStore = [persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:storeOptions
error:&addStoreError];
if (!persistentStore) {
return addStoreError;
}
// exclude the SQLite database from iCloud Backups per the iCloud Data Storage Guidelines
NSError *fileAttrError = nil;
if (![storeURL setResourceValue:@(YES) forKey:NSURLIsExcludedFromBackupKey error:&fileAttrError]) {
NSLog(@"Failed to exclude item at path '%@' from Backup: %@", dbPath, fileAttrError);
}
// set up the managed object contexts ------------------------------------------------------------------------------
// our primary MOC is a private queue concurrency type
NSManagedObjectContext *privateContext
= [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
privateContext.persistentStoreCoordinator = persistentStoreCoordinator;
privateContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy;
objc_setAssociatedObject(self, &PersistentStoreManagedObjectContextKey, privateContext, OBJC_ASSOCIATION_RETAIN);
// mreate an MOC for use on the main queue
NSManagedObjectContext *mainContext
= [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
mainContext.parentContext = privateContext;
mainContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy;
objc_setAssociatedObject(self, &MainQueueManagedObjectKey, mainContext, OBJC_ASSOCIATION_RETAIN);
// merge changes from a primary MOC back into the main queue when complete
ManagedObjectContextChangeMergingObserver *observer
= [[ManagedObjectContextChangeMergingObserver alloc] initWithObservedContext:privateContext
mergeContext:mainContext];
objc_setAssociatedObject(mainContext,
&ManagedObjectContextChangeMergingObserverKey,
observer,
OBJC_ASSOCIATION_RETAIN);
// and finally, we're done
initialized = YES;
return nil;
}
+ (NSManagedObjectContext *)persistentStoreManagedObjectContext
{
NSAssert(initialized, @"must be initialized before that");
return objc_getAssociatedObject([ManagedObject class], &PersistentStoreManagedObjectContextKey);
}
+ (NSManagedObjectContext *)mainQueueManagedObjectContext
{
NSAssert(initialized, @"must be initalized before you do that");
return objc_getAssociatedObject([ManagedObject class], &MainQueueManagedObjectKey);
}
+ (NSFetchRequest *)fetchRequest
{
return [NSFetchRequest fetchRequestWithEntityName:[self entityName]];
}
+ (NSString *)entityName
{
return NSStringFromClass(self);
}
+ (NSArray *)fetchObjects:(NSFetchRequest *)request managedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
__block NSArray *ret = nil;
__block NSError *error = nil;
[managedObjectContext performBlockAndWait:^{
ret = [managedObjectContext executeFetchRequest:request error:&error];
if (!ret) {
NSLog(@"%s:%d %s: %@", __FILE__, __LINE__, __FUNCTION__, error);
}
}];
return ret;
}
+ (NSArray *)objectsMatchingInContext:(NSManagedObjectContext *)managedObjectContext
format:(NSString *)predicateFormat, ...
{
NSArray *a;
va_list args;
va_start(args, predicateFormat);
a = [self objectsMatchingInContext:managedObjectContext format:predicateFormat arguments:args];
va_end(args);
return a;
}
+ (NSArray *)objectsMatchingInContext:(NSManagedObjectContext *)managedObjectContext
format:(NSString *)predicateFormat
arguments:(va_list)args
{
NSFetchRequest *request = [self fetchRequest];
request.predicate = [NSPredicate predicateWithFormat:predicateFormat arguments:args];
return [self fetchObjects:request managedObjectContext:managedObjectContext];
}
+ (void)deleteAllObjectsOnComplete:(void(^)(NSError *errorSaving))onFinish
waitForFinish:(BOOL)blockOnCallingThread
managedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
void (^theBlock)(void) = ^{
NSFetchRequest *req = [self fetchRequest];
NSArray *matches = [self fetchObjects:req managedObjectContext:managedObjectContext];
for (NSManagedObject *obj in matches) {
[managedObjectContext deleteObject:obj];
}
NSError *error = nil;
if (![managedObjectContext save:&error]) {
NSLog(@"Error saving context while deleting objects: %@", error);
}
if (onFinish) {
onFinish(error);
}
};
if (blockOnCallingThread) {
[managedObjectContext performBlockAndWait:theBlock];
} else {
[managedObjectContext performBlock:theBlock];
}
}
+ (void)deleteAllObjectsAndWait
{
[self deleteAllObjectsOnComplete:nil waitForFinish:YES
managedObjectContext:[self persistentStoreManagedObjectContext]];
}
#pragma mark - HTTP Mapping Stuff
//----------------------------------------------------------------------------------------------------------------------
+ (NSMutableURLRequest *)getRequestPath:(NSString *)path method:(NSString *)method options:(NSDictionary *)options
{
HTTPClient *client = [HTTPClient sharedClient];
NSDictionary *params = @{};
if (options[IMPRequstGETParameters]) {
NSParameterAssert([options[IMPRequstGETParameters] isKindOfClass:[NSDictionary class]]);
params = options[IMPRequstGETParameters];
}
NSMutableURLRequest *req = [client requestWithMethod:method path:path parameters:params];
[req addValue:@"application/json" forHTTPHeaderField:@"Accept"];
req.options = options;
return req;
}
+ (void)doRequest:(NSMutableURLRequest *)originalRequest
success:(HTTPRequestSuccessBlock)success
failure:(HTTPRequestErrorBlock)failure
{
HTTPClient *client = [HTTPClient sharedClient];
// __block AFJSONRequestOperation *op = nil;
//
//
// op = [AFJSONRequestOperation JSONRequestOperationWithRequest:originalRequest success:
// ^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
[client doRequest:originalRequest success:^(AFHTTPRequestOperation *op, id result) {
NSManagedObjectContext *ctx = [self persistentStoreManagedObjectContext];
[ctx performBlock:^{
id iterObj = result;
if ([result isKindOfClass:[NSDictionary class]]) {
iterObj = @[result]; // this may come in as a single object
} else if (![iterObj isKindOfClass:[NSArray class]]) {
// uh, well it's not an array or a dictionary so i don't know what to do
return failure(op, result, [NSError errorWithDomain:@"" code:400 userInfo:
@{NSLocalizedDescriptionKey: NSLocalizedString(@"Unknown response, expecting array or dictionary.", nil)}]);
}
// clear out the previous collection if we're told to do so
if (originalRequest.options[IMPClearCollectionBeforeSaving]) {
NSFetchRequest *req = [self fetchRequest];
NSMutableSet *incommingUrls = [NSMutableSet set];
for (id obj in iterObj) {
if (obj[@"url"]) {
[incommingUrls addObject:obj[@"url"]];
}
}
req.predicate = [NSPredicate predicateWithFormat:@"NOT (url IN %@)", incommingUrls];
NSArray *staleObjects = [self fetchObjects:req managedObjectContext:ctx];
for (ManagedObject *obj in staleObjects) {
[ctx deleteObject:obj];
}
}
NSMutableArray *ret = [NSMutableArray arrayWithCapacity:[iterObj count]];
[iterObj enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
[ret addObject:[self fromJSON:obj managedObjectContext:ctx]];
}];
NSError *error = nil;
if (![ctx save:&error]) {
dispatch_async(op.successCallbackQueue ?: dispatch_get_main_queue(), ^{
failure(op, result, error);
});
} else {
dispatch_async(op.successCallbackQueue ?: dispatch_get_main_queue(), ^{
success(op, ret);
});
}
}];
} failure:failure];
// op.successCallbackQueue = dispatch_get_main_queue();
// [client enqueueHTTPRequestOperation:op];
}
+ (instancetype)fromURL:(NSString *)url managedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
NSArray *objs = [self objectsMatchingInContext:managedObjectContext format:@"url = %@", url];
if (objs.count) {
return objs[0];
}
return nil;
}
+ (instancetype)fromJSON:(NSDictionary *)data managedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
NSParameterAssert(data[@"url"]);
ManagedObject *obj = [self fromURL:data[@"url"] managedObjectContext:managedObjectContext];
if (!obj) {
obj = [NSEntityDescription insertNewObjectForEntityForName:[self entityName]
inManagedObjectContext:managedObjectContext];
}
[obj updateWithJSON:data managedObjectContext:managedObjectContext];
return obj;
}
- (void)updateWithJSON:(NSDictionary *)data managedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
[self setUrl:data[@"url"]];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment