Created
December 6, 2013 19:46
-
-
Save samuraisam/7831026 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // 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