Skip to content

Instantly share code, notes, and snippets.

@bcbroom
Last active August 29, 2015 13:56
Show Gist options
  • Select an option

  • Save bcbroom/8983588 to your computer and use it in GitHub Desktop.

Select an option

Save bcbroom/8983588 to your computer and use it in GitHub Desktop.
Core Data Helper Class
#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
#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