Skip to content

Instantly share code, notes, and snippets.

@somtd
Created December 7, 2013 10:03
Show Gist options
  • Save somtd/7839262 to your computer and use it in GitHub Desktop.
Save somtd/7839262 to your computer and use it in GitHub Desktop.
Synchronize CoreData with Parse #BLOG
#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