Created
December 7, 2013 10:03
-
-
Save somtd/7839262 to your computer and use it in GitHub Desktop.
Synchronize CoreData with Parse #BLOG
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
#import "SyncEngine.h" | |
#import "CoreDataController.h" | |
#import "ParseApiClient.h" | |
#import "AFHTTPRequestOperation.h" | |
NSString * const kSDSyncEngineInitialCompleteKey = @"SDSyncEngineInitialSyncCompleted"; | |
NSString * const kSDSyncEngineSyncCompletedNotificationName = @"SDSyncEngineSyncCompleted"; | |
NSString * const kSDSyncEngineDownloadCompleteNotification = @"SDSyncEngineDownloadComplete"; | |
@interface SyncEngine () | |
@property (nonatomic, strong) NSMutableArray *registeredClassesToSync; | |
@property (nonatomic, strong) NSDateFormatter *dateFormatter; | |
@end | |
@implementation SyncEngine | |
+ (SyncEngine *)sharedEngine { | |
static SyncEngine *sharedEngine = nil; | |
static dispatch_once_t onceToken; | |
dispatch_once(&onceToken, ^{ | |
sharedEngine = [[SyncEngine alloc] init]; | |
}); | |
return sharedEngine; | |
} | |
- (void)registerNSManagedObjectClassToSync:(Class)aClass { | |
if (!self.registeredClassesToSync) { | |
self.registeredClassesToSync = [NSMutableArray array]; | |
} | |
if ([aClass isSubclassOfClass:[NSManagedObject class]]) { | |
if (![self.registeredClassesToSync containsObject:NSStringFromClass(aClass)]) { | |
[self.registeredClassesToSync addObject:NSStringFromClass(aClass)]; | |
} else { | |
DLog(@"Unable to register %@ as it is already registered", NSStringFromClass(aClass)); | |
} | |
} else { | |
DLog(@"Unable to register %@ as it is not a subclass of NSManagedObject", NSStringFromClass(aClass)); | |
} | |
} | |
- (void)startSync { | |
if (!self.syncInProgress) { | |
[self willChangeValueForKey:@"syncInProgress"]; | |
_syncInProgress = YES; | |
[self didChangeValueForKey:@"syncInProgress"]; | |
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ | |
[self downloadDataForRegisteredObjects:YES]; | |
}); | |
} | |
} | |
- (void)executeSyncCompletedOperations { | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
[self setInitialSyncCompleted]; | |
NSError *error = nil; | |
[[CoreDataController sharedInstance] saveBackgroundContext]; | |
if (error) { | |
DLog(@"Error saving background context after creating objects on server: %@", error); | |
} | |
[[CoreDataController sharedInstance] saveMasterContext]; | |
[[NSNotificationCenter defaultCenter] | |
postNotificationName:kSDSyncEngineSyncCompletedNotificationName | |
object:nil]; | |
[self willChangeValueForKey:@"syncInProgress"]; | |
_syncInProgress = NO; | |
[self didChangeValueForKey:@"syncInProgress"]; | |
}); | |
} | |
- (BOOL)initialSyncComplete { | |
return [[[NSUserDefaults standardUserDefaults] valueForKey:kSDSyncEngineInitialCompleteKey] boolValue]; | |
} | |
- (void)setInitialSyncCompleted { | |
[[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithBool:YES] forKey:kSDSyncEngineInitialCompleteKey]; | |
[[NSUserDefaults standardUserDefaults] synchronize]; | |
} | |
- (NSDate *)mostRecentUpdatedAtDateForEntityWithName:(NSString *)entityName { | |
__block NSDate *date = nil; | |
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:entityName]; | |
[request setSortDescriptors:[NSArray arrayWithObject: | |
[NSSortDescriptor sortDescriptorWithKey:@"updatedAt" ascending:NO]]]; | |
[request setFetchLimit:1]; | |
[[[CoreDataController sharedInstance] backgroundManagedObjectContext] performBlockAndWait:^{ | |
NSError *error = nil; | |
NSArray *results = [[[CoreDataController sharedInstance] backgroundManagedObjectContext] executeFetchRequest:request error:&error]; | |
if ([results lastObject]) { | |
date = [[results lastObject] valueForKey:@"updatedAt"]; | |
} | |
}]; | |
return date; | |
} | |
- (void)downloadDataForRegisteredObjects:(BOOL)useUpdatedAtDate { | |
NSMutableArray *operations = [NSMutableArray array]; | |
for (NSString *className in self.registeredClassesToSync) { | |
NSDate *mostRecentUpdatedDate = nil; | |
if (useUpdatedAtDate) { | |
mostRecentUpdatedDate = [self mostRecentUpdatedAtDateForEntityWithName:className]; | |
} | |
NSMutableURLRequest *request = [[ParseApiClient sharedClient] | |
GETRequestForAllRecordsOfClass:className | |
updatedAfterDate:mostRecentUpdatedDate]; | |
AFHTTPRequestOperation *operation = [[ParseApiClient sharedClient] HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id responseObject) { | |
AFJSONRequestOperation *JSONRequest = (AFJSONRequestOperation *)operation; | |
NSDictionary *responseDictionary = JSONRequest.responseJSON; | |
if ([responseDictionary isKindOfClass:[NSDictionary class]]) { | |
DLog(@"Response for %@: %@", className, responseDictionary); | |
NSDictionary *responseInfo = @{@"className": className,@"responseDictionary":responseDictionary}; | |
[self writeJSONResponse:responseDictionary toDiskForClassWithName:className]; | |
[[NSNotificationCenter defaultCenter] | |
postNotificationName:kSDSyncEngineDownloadCompleteNotification | |
object:self | |
userInfo:responseInfo]; | |
} | |
} failure:^(AFHTTPRequestOperation *operation, NSError *error) { | |
DLog(@"Request for class %@ failed with error: %@", className, error); | |
}]; | |
[operations addObject:operation]; | |
} | |
[[ParseApiClient sharedClient] enqueueBatchOfHTTPRequestOperations:operations progressBlock:^(NSUInteger numberOfCompletedOperations, NSUInteger totalNumberOfOperations) { | |
} completionBlock:^(NSArray *operations) { | |
if (useUpdatedAtDate) { | |
[self processJSONDataRecordsIntoCoreData]; | |
} | |
// else { | |
// [self processJSONDataRecordsForDeletion]; | |
// } | |
}]; | |
} | |
- (void)processJSONDataRecordsIntoCoreData { | |
NSManagedObjectContext *managedObjectContext = [[CoreDataController sharedInstance] backgroundManagedObjectContext]; | |
for (NSString *className in self.registeredClassesToSync) { | |
if (![self initialSyncComplete]) { // import all downloaded data to Core Data for initial sync | |
NSDictionary *JSONDictionary = [self JSONDictionaryForClassWithName:className]; | |
NSArray *records = [JSONDictionary objectForKey:@"results"]; | |
for (NSDictionary *record in records) { | |
[self newManagedObjectWithClassName:className forRecord:record]; | |
} | |
} else { | |
NSArray *downloadedRecords = [self JSONDataRecordsForClass:className sortedByKey:@"objectId"]; | |
if ([downloadedRecords lastObject]) { | |
NSArray *storedRecords = [self managedObjectsForClass:className sortedByKey:@"objectId" usingArrayOfIds:[downloadedRecords valueForKey:@"objectId"] inArrayOfIds:YES]; | |
int currentIndex = 0; | |
for (NSDictionary *record in downloadedRecords) { | |
NSManagedObject *storedManagedObject = nil; | |
if ([storedRecords count] > currentIndex) { | |
storedManagedObject = [storedRecords objectAtIndex:currentIndex]; | |
} | |
if ([[storedManagedObject valueForKey:@"objectId"] isEqualToString:[record valueForKey:@"objectId"]]) { | |
[self updateManagedObject:[storedRecords objectAtIndex:currentIndex] withRecord:record]; | |
} else { | |
[self newManagedObjectWithClassName:className forRecord:record]; | |
} | |
currentIndex++; | |
} | |
} | |
} | |
[managedObjectContext performBlockAndWait:^{ | |
NSError *error = nil; | |
if (![managedObjectContext save:&error]) { | |
DLog(@"Unable to save context for class %@", className); | |
} | |
}]; | |
[self deleteJSONDataRecordsForClassWithName:className]; | |
[self executeSyncCompletedOperations]; | |
} | |
[self downloadDataForRegisteredObjects:NO]; | |
} | |
- (void)newManagedObjectWithClassName:(NSString *)className forRecord:(NSDictionary *)record { | |
NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:className inManagedObjectContext:[[CoreDataController sharedInstance] backgroundManagedObjectContext]]; | |
[record enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { | |
[self setValue:obj forKey:key forManagedObject:newManagedObject]; | |
}]; | |
[record setValue:[NSNumber numberWithInt:ObjectSynced] forKey:@"syncStatus"]; | |
} | |
- (void)updateManagedObject:(NSManagedObject *)managedObject withRecord:(NSDictionary *)record { | |
[record enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { | |
[self setValue:obj forKey:key forManagedObject:managedObject]; | |
}]; | |
} | |
- (void)setValue:(id)value forKey:(NSString *)key forManagedObject:(NSManagedObject *)managedObject { | |
if ([key isEqualToString:@"createdAt"] || [key isEqualToString:@"updatedAt"]) { | |
NSDate *date = [self dateUsingStringFromAPI:value]; | |
[managedObject setValue:date forKey:key]; | |
} else if ([value isKindOfClass:[NSDictionary class]]) { | |
if ([value objectForKey:@"__type"]) { | |
NSString *dataType = [value objectForKey:@"__type"]; | |
if ([dataType isEqualToString:@"Date"]) { | |
NSString *dateString = [value objectForKey:@"iso"]; | |
NSDate *date = [self dateUsingStringFromAPI:dateString]; | |
[managedObject setValue:date forKey:key]; | |
} else if ([dataType isEqualToString:@"File"]) { | |
NSString *urlString = [value objectForKey:@"url"]; | |
NSURL *url = [NSURL URLWithString:urlString]; | |
NSURLRequest *request = [NSURLRequest requestWithURL:url]; | |
NSURLResponse *response = nil; | |
NSError *error = nil; | |
NSData *dataResponse = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]; | |
[managedObject setValue:dataResponse forKey:key]; | |
} else { | |
DLog(@"Unknown Data Type Received"); | |
[managedObject setValue:nil forKey:key]; | |
} | |
} | |
} else { | |
[managedObject setValue:value forKey:key]; | |
} | |
} | |
- (NSArray *)managedObjectsForClass:(NSString *)className withSyncStatus:(ObjectSyncStatus)syncStatus { | |
__block NSArray *results = nil; | |
NSManagedObjectContext *managedObjectContext = [[CoreDataController sharedInstance] backgroundManagedObjectContext]; | |
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:className]; | |
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"syncStatus = %d", syncStatus]; | |
[fetchRequest setPredicate:predicate]; | |
[managedObjectContext performBlockAndWait:^{ | |
NSError *error = nil; | |
results = [managedObjectContext executeFetchRequest:fetchRequest error:&error]; | |
}]; | |
return results; | |
} | |
- (NSArray *)managedObjectsForClass:(NSString *)className sortedByKey:(NSString *)key usingArrayOfIds:(NSArray *)idArray inArrayOfIds:(BOOL)inIds { | |
__block NSArray *results = nil; | |
NSManagedObjectContext *managedObjectContext = [[CoreDataController sharedInstance] backgroundManagedObjectContext]; | |
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:className]; | |
NSPredicate *predicate; | |
if (inIds) { | |
predicate = [NSPredicate predicateWithFormat:@"objectId IN %@", idArray]; | |
} else { | |
predicate = [NSPredicate predicateWithFormat:@"NOT (objectId IN %@)", idArray]; | |
} | |
[fetchRequest setPredicate:predicate]; | |
[fetchRequest setSortDescriptors:[NSArray arrayWithObject: | |
[NSSortDescriptor sortDescriptorWithKey:@"objectId" ascending:YES]]]; | |
[managedObjectContext performBlockAndWait:^{ | |
NSError *error = nil; | |
results = [managedObjectContext executeFetchRequest:fetchRequest error:&error]; | |
}]; | |
return results; | |
} | |
- (void)initializeDateFormatter { | |
if (!self.dateFormatter) { | |
self.dateFormatter = [[NSDateFormatter alloc] init]; | |
[self.dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"]; | |
[self.dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"GMT"]]; | |
} | |
} | |
- (NSDate *)dateUsingStringFromAPI:(NSString *)dateString { | |
[self initializeDateFormatter]; | |
// NSDateFormatter does not like ISO 8601 so strip the milliseconds and timezone | |
dateString = [dateString substringWithRange:NSMakeRange(0, [dateString length]-5)]; | |
return [self.dateFormatter dateFromString:dateString]; | |
} | |
- (NSString *)dateStringForAPIUsingDate:(NSDate *)date { | |
[self initializeDateFormatter]; | |
NSString *dateString = [self.dateFormatter stringFromDate:date]; | |
// remove Z | |
dateString = [dateString substringWithRange:NSMakeRange(0, [dateString length]-1)]; | |
// add milliseconds and put Z back on | |
dateString = [dateString stringByAppendingFormat:@".000Z"]; | |
return dateString; | |
} | |
#pragma mark - File Management | |
- (NSURL *)applicationCacheDirectory | |
{ | |
return [[[NSFileManager defaultManager] URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject]; | |
} | |
- (NSURL *)JSONDataRecordsDirectory{ | |
NSFileManager *fileManager = [NSFileManager defaultManager]; | |
NSURL *url = [NSURL URLWithString:@"JSONRecords/" relativeToURL:[self applicationCacheDirectory]]; | |
NSError *error = nil; | |
if (![fileManager fileExistsAtPath:[url path]]) { | |
[fileManager createDirectoryAtPath:[url path] withIntermediateDirectories:YES attributes:nil error:&error]; | |
} | |
return url; | |
} | |
- (void)writeJSONResponse:(id)response toDiskForClassWithName:(NSString *)className { | |
NSURL *fileURL = [NSURL URLWithString:className relativeToURL:[self JSONDataRecordsDirectory]]; | |
if (![(NSDictionary *)response writeToFile:[fileURL path] atomically:YES]) { | |
DLog(@"Error saving response to disk, will attempt to remove NSNull values and try again."); | |
// remove NSNulls and try again... | |
NSArray *records = [response objectForKey:@"results"]; | |
NSMutableArray *nullFreeRecords = [NSMutableArray array]; | |
for (NSDictionary *record in records) { | |
NSMutableDictionary *nullFreeRecord = [NSMutableDictionary dictionaryWithDictionary:record]; | |
[record enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { | |
if ([obj isKindOfClass:[NSNull class]]) { | |
[nullFreeRecord setValue:nil forKey:key]; | |
} | |
}]; | |
[nullFreeRecords addObject:nullFreeRecord]; | |
} | |
NSDictionary *nullFreeDictionary = [NSDictionary dictionaryWithObject:nullFreeRecords forKey:@"results"]; | |
if (![nullFreeDictionary writeToFile:[fileURL path] atomically:YES]) { | |
DLog(@"Failed all attempts to save reponse to disk: %@", response); | |
} | |
} | |
} | |
- (void)deleteJSONDataRecordsForClassWithName:(NSString *)className { | |
NSURL *url = [NSURL URLWithString:className relativeToURL:[self JSONDataRecordsDirectory]]; | |
NSError *error = nil; | |
BOOL deleted = [[NSFileManager defaultManager] removeItemAtURL:url error:&error]; | |
if (!deleted) { | |
DLog(@"Unable to delete JSON Records at %@, reason: %@", url, error); | |
} | |
} | |
- (NSDictionary *)JSONDictionaryForClassWithName:(NSString *)className { | |
NSURL *fileURL = [NSURL URLWithString:className relativeToURL:[self JSONDataRecordsDirectory]]; | |
return [NSDictionary dictionaryWithContentsOfURL:fileURL]; | |
} | |
- (NSArray *)JSONDataRecordsForClass:(NSString *)className sortedByKey:(NSString *)key { | |
NSDictionary *JSONDictionary = [self JSONDictionaryForClassWithName:className]; | |
NSArray *records = [JSONDictionary objectForKey:@"results"]; | |
return [records sortedArrayUsingDescriptors:[NSArray arrayWithObject: | |
[NSSortDescriptor sortDescriptorWithKey:key ascending:YES]]]; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment