Last active
August 29, 2015 13:56
-
-
Save bcbroom/8983588 to your computer and use it in GitHub Desktop.
Core Data Helper Class
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 <Foundation/Foundation.h> | |
| #import <CoreData/CoreData.h> | |
| @interface CoreDataHelper :NSObject | |
| @property (nonatomic, readonly) NSManagedObjectContext *context; | |
| @property (nonatomic, readonly) NSManagedObjectModel *model; | |
| @property (nonatomic, readonly) NSPersistentStoreCoordinator *coordinator; | |
| @property (nonatomic, readonly) NSPersistentStore *store; | |
| - (void)setupCoreData; | |
| - (void)saveContext; | |
| @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
| #import "CoreDataHelper.h" | |
| @implementation CoreDataHelper | |
| #define debug 1 | |
| #pragma mark - FILES | |
| NSString *storeFilename = @"AppName.sqlite"; | |
| #pragma mark - PATHS | |
| - (NSString *)applicationDocumentsDirectory { | |
| if (debug==1) { | |
| NSLog(@"Running %@ '%@'", self.class,NSStringFromSelector(_cmd)); | |
| } | |
| return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,YES) lastObject]; | |
| } | |
| - (NSURL *)applicationStoresDirectory { | |
| if (debug==1) { | |
| NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd)); | |
| } | |
| NSURL *storesDirectory = [[NSURL fileURLWithPath:[self applicationDocumentsDirectory]] URLByAppendingPathComponent:@"Stores"]; | |
| NSFileManager *fileManager = [NSFileManager defaultManager]; | |
| if (![fileManager fileExistsAtPath:[storesDirectory path]]) { | |
| NSError *error = nil; | |
| if ([fileManager createDirectoryAtURL:storesDirectory withIntermediateDirectories:YES attributes:nil error:&error]) { | |
| if (debug==1) { | |
| NSLog(@"Successfully created Stores directory"); | |
| } else { | |
| NSLog(@"Failed to create stores directory: %@", error); | |
| } | |
| } | |
| } | |
| return storesDirectory; | |
| } | |
| - (NSURL *)storeURL{ | |
| if (debug==1) { | |
| NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd)); | |
| } | |
| return [[self applicationStoresDirectory] URLByAppendingPathComponent:storeFilename]; | |
| } | |
| #pragma mark - Setup | |
| - (id)init { | |
| if (debug==1) { | |
| NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd)); | |
| } | |
| self = [super init]; | |
| if (!self) { | |
| return nil; | |
| } | |
| _model = [NSManagedObjectModel mergedModelFromBundles:nil]; | |
| _coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:_model]; | |
| _context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; | |
| [_context setPersistentStoreCoordinator:_coordinator]; | |
| return self; | |
| } | |
| - (void)loadStore { | |
| if (debug==1) { | |
| NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd)); | |
| } | |
| if (_store) {return;} // Don't load store if it's already loaded | |
| BOOL useMigrationManager = NO; | |
| if (useMigrationManager && [self isMigrationNecessaryForStore:[self storeURL]]) { | |
| [self performBackgroundManagedMigrationForStore:[self storeURL]]; | |
| } else { | |
| // for testing, set NSInferMappingModelAutomaticallyOption to NO so that errors are more obvious | |
| // for production, set to YES to prevent crashes | |
| NSDictionary *options = @{ | |
| NSMigratePersistentStoresAutomaticallyOption: @YES, | |
| NSInferMappingModelAutomaticallyOption: @YES, | |
| NSSQLitePragmasOption: @{@"journal_mode": @"DELETE"} | |
| }; | |
| NSError *error = nil; | |
| _store = [_coordinator addPersistentStoreWithType:NSSQLiteStoreType | |
| configuration:nil | |
| URL:[self storeURL] | |
| options:options | |
| error:&error]; | |
| if (!_store) { NSLog(@"Failed to add store. Error: %@", error); abort(); } | |
| else { if (debug==1) { NSLog(@"Successfully added store: %@", _store); } } | |
| } | |
| } | |
| - (void)setupCoreData { | |
| if (debug==1) { | |
| NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd)); | |
| } | |
| [self loadStore]; | |
| } | |
| #pragma mark - Saving | |
| - (void)saveContext { | |
| if (debug==1) { | |
| NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd)); | |
| } | |
| if ([_context hasChanges]) { | |
| NSError *error = nil; | |
| if ([_context save:&error]) { | |
| NSLog(@"_context Saved changes to persistent store"); | |
| } else { | |
| NSLog(@"Failed to save _context: %@", error); | |
| [self showValidationError:error]; | |
| } | |
| } else { | |
| NSLog(@"Skipped _context save, there are no changes."); | |
| } | |
| } | |
| #pragma mark - Migration Manager | |
| - (BOOL)isMigrationNecessaryForStore:(NSURL *)storeUrl { | |
| if (debug==1) { | |
| NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd)); | |
| } | |
| if (![[NSFileManager defaultManager] fileExistsAtPath:[self storeURL].path]) { | |
| if (debug==1) {NSLog(@"Skipped migration: Source database missing.");} | |
| return NO; | |
| } | |
| NSError *error = nil; | |
| NSDictionary *sourceMetadata = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType URL:storeUrl error:&error]; | |
| NSManagedObjectModel *destinationModel = _coordinator.managedObjectModel; | |
| if ([destinationModel isConfiguration:nil compatibleWithStoreMetadata:sourceMetadata]) { | |
| if (debug==1) { | |
| NSLog(@"Skipped Migration: source is already compatible"); | |
| } | |
| return NO; | |
| } | |
| return YES; | |
| } | |
| - (BOOL)migrateStore:(NSURL *)sourceStore { | |
| if (debug==1) { | |
| NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd)); | |
| } | |
| BOOL success = NO; | |
| NSError *error = nil; | |
| // Step 1 - Gather the source, destination, and mapping model | |
| NSDictionary *sourceMetadata = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType URL:sourceStore error:&error]; | |
| NSManagedObjectModel *sourceModel = [NSManagedObjectModel mergedModelFromBundles:nil forStoreMetadata:sourceMetadata]; | |
| NSManagedObjectModel *destinationModel = _model; | |
| NSMappingModel *mappingModel = [NSMappingModel mappingModelFromBundles:nil forSourceModel:sourceModel destinationModel:destinationModel]; | |
| // Step 2 - Perform migration, assuming the mapping model isn't null | |
| if (mappingModel) { | |
| NSError *error = nil; | |
| NSMigrationManager *migrationManager = [[NSMigrationManager alloc] initWithSourceModel:sourceModel destinationModel:destinationModel]; | |
| [migrationManager addObserver:self forKeyPath:@"migrationProgress" options:NSKeyValueObservingOptionNew context:NULL]; | |
| NSURL *destinationStore = [[self applicationStoresDirectory] URLByAppendingPathComponent:@"Temp.sqlite"]; | |
| success = [migrationManager migrateStoreFromURL:sourceStore | |
| type:NSSQLiteStoreType | |
| options:nil | |
| withMappingModel:mappingModel | |
| toDestinationURL:destinationStore | |
| destinationType:NSSQLiteStoreType | |
| destinationOptions:nil | |
| error:&error]; | |
| if (success) { | |
| // Step 3 - Replace the old store with the new migrated store | |
| if ([self replaceStore:sourceStore withStore:destinationStore]) { | |
| if (debug==1) { NSLog(@"Successfully migrated %@ to the current model", sourceStore.path); } | |
| [migrationManager removeObserver:self forKeyPath:@"migrationProgress"]; | |
| } | |
| } else { | |
| if (debug==1) { NSLog(@"Failed migration: %@", error); } | |
| } | |
| } else { | |
| if (debug==1) { NSLog(@"Failed migration: Mapping model is null"); } | |
| } | |
| return YES; // indicates migration is finished, not that it is successfull. | |
| } | |
| - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { | |
| if ([keyPath isEqualToString:@"migrationProgress"]) { | |
| dispatch_async(dispatch_get_main_queue(), ^{ | |
| float progress = [[change objectForKey:NSKeyValueChangeNewKey] floatValue]; | |
| self.migrationVC.progressView.progress = progress; | |
| int percentage = progress * 100; | |
| NSString *string = [NSString stringWithFormat:@"Migration Progress: %i%%", percentage]; | |
| NSLog(@"%@", string); | |
| self.migrationVC.label.text = string; | |
| }); | |
| } | |
| } | |
| - (BOOL)replaceStore:(NSURL *)old withStore:(NSURL *)new { | |
| BOOL success = NO; | |
| NSError *error = nil; | |
| if ([[NSFileManager defaultManager] removeItemAtURL:old error:&error]) { | |
| error = nil; | |
| if ([[NSFileManager defaultManager] moveItemAtURL:new toURL:old error:&error]) { | |
| success = YES; | |
| } else { | |
| if (debug==1) { NSLog(@"Failed to re-home new store %@", error); } | |
| } | |
| } else { | |
| if (debug==1) { NSLog(@"Failed to remove old store %@: Error: %@", old, error); } | |
| } | |
| return success; | |
| } | |
| - (void)performBackgroundManagedMigrationForStore:(NSURL *)storeURL { | |
| if (debug==1) { | |
| NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd)); | |
| } | |
| // Show migration progress view preventing the user from using the app | |
| UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil]; | |
| self.migrationVC = [sb instantiateViewControllerWithIdentifier:@"migration"]; | |
| UIApplication *sa = [UIApplication sharedApplication]; | |
| UINavigationController *nc = (UINavigationController *)sa.keyWindow.rootViewController; | |
| [nc presentViewController:self.migrationVC animated:NO completion:nil]; | |
| // Perform migration in the background, so it doesn't freeze the UI. | |
| // This way progress can be shown to the user | |
| dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ | |
| BOOL done = [self migrateStore:storeURL]; | |
| if (done) { | |
| // When migration finishes, add the newly migrated store | |
| dispatch_async(dispatch_get_main_queue(), ^{ | |
| NSError *error = nil; | |
| _store = [_coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[self storeURL] options:nil error:&error]; | |
| if (!_store) { NSLog(@"Failed to add a migrated store. Error: %@", error); abort(); } | |
| else { NSLog(@"Successfully added a migrated store: %@", _store); } | |
| [self.migrationVC dismissViewControllerAnimated:NO completion:nil]; | |
| self.migrationVC = nil; | |
| }); | |
| } | |
| }); | |
| } | |
| #pragma mark - Validation Error Handling | |
| - (void)showValidationError:(NSError *)anError { | |
| if (anError && [anError.domain isEqualToString:@"NSCocoaErrorDomain"]) { | |
| NSArray *errors = nil; // holds all errors | |
| NSString *txt = @""; // the error message text of the alert | |
| // populate array with error(s) | |
| if (anError.code == NSValidationMultipleErrorsError) { | |
| errors = [anError.userInfo objectForKey:NSDetailedErrorsKey]; | |
| } else { | |
| errors = [NSArray arrayWithObject:anError]; | |
| } | |
| // display the error(s) | |
| if (errors && errors.count > 0) { | |
| // build error message text based on errors | |
| for (NSError *error in errors) { | |
| NSString *entity = [[[error.userInfo objectForKey:@"NSValidationErrorObject"] entity] name]; | |
| NSString *property = [error.userInfo objectForKey:@"NSValidationErrorKey"]; | |
| switch (error.code) { | |
| case NSValidationRelationshipDeniedDeleteError: | |
| txt = [txt stringByAppendingFormat:@"%@ delete was denied because there are associated %@\n(Error Code %li)\n\n", entity, property, (long)error.code]; | |
| break; | |
| default: | |
| txt = [txt stringByAppendingFormat:@"Unhandled error code %li in showValidationError method", (long)error.code]; | |
| break; | |
| } | |
| } | |
| // display error message txt message | |
| UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Validation Error" | |
| message:[NSString stringWithFormat:@"%@Please double-tap the home button and close this application by swiping the application screenshot upwards", txt] | |
| delegate:nil cancelButtonTitle:nil otherButtonTitles:nil]; | |
| [alertView show]; | |
| } | |
| } | |
| } | |
| @end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment