| // |
| // SMStoreController.m |
| // StoreMad |
| // |
| // Created by Andrew Smith on 7/20/12. |
| // Copyright (c) 2012 Andrew B. Smith ( http://github.com/drewsmits ). All rights reserved. |
| // |
| // Permission is hereby granted, free of charge, to any person obtaining a copy |
| // of this software and associated documentation files (the "Software"), to deal |
| // in the Software without restriction, including without limitation the rights |
| // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies |
| // of the Software, and to permit persons to whom the Software is furnished to do so, |
| // subject to the following conditions: |
| // |
| // The above copyright notice and this permission notice shall be included |
| // in all copies or substantial portions of the Software. |
| // |
| // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| // |
| |
| #import "SMStoreController.h" |
| |
| #import <UIKit/UIApplication.h> |
| #import "NSManagedObjectContext+StoreMad.h" |
| |
| NSString * const kSMStoreControllerDidReset = @"kSMStoreControllerDidReset"; |
| |
| @interface SMStoreController () |
| |
| @property (nonatomic, copy, readwrite) NSURL *storeURL; |
| @property (nonatomic, copy, readwrite) NSURL *modelURL; |
| @property (nonatomic, copy, readwrite) NSURL *oldModelURL; |
| |
| @property (nonatomic, strong, readwrite) NSManagedObjectContext *managedObjectContext; |
| @property (nonatomic, strong, readwrite) NSManagedObjectModel *managedObjectModel; |
| @property (nonatomic, strong, readwrite) NSPersistentStoreCoordinator *persistentStoreCoordinator; |
| |
| @end |
| |
| @implementation SMStoreController |
| |
| - (void)dealloc |
| { |
| [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| } |
| |
| + (SMStoreController *)storeControllerWithStoreURL:(NSURL *)storeURL |
| andModelURL:(NSURL *)modelURL |
| oldModelURL:(NSURL *)oldModelURL |
| { |
| NSParameterAssert(storeURL); |
| NSParameterAssert(modelURL); |
| |
| SMStoreController *controller = [self new]; |
| |
| controller.oldModelURL = oldModelURL; |
| controller.storeURL = storeURL; |
| controller.modelURL = modelURL; |
| |
| // Force lazy load |
| [controller managedObjectContext]; |
| |
| return controller; |
| } |
| |
| |
| #pragma mark |
| |
| - (void)reset |
| { |
| @synchronized(self) { |
| // Nil local variables |
| _managedObjectContext = nil; |
| _managedObjectModel = nil; |
| _persistentStoreCoordinator = nil; |
| |
| // Delete SQlite |
| [self deleteStore]; |
| |
| // Rebuild |
| [self managedObjectContext]; |
| } |
| } |
| |
| - (void)deleteStore |
| { |
| @synchronized(self) { |
| // Ensure we are on the main thread |
| NSAssert([[NSThread currentThread] isEqual:[NSThread mainThread]], @"Delete operation must occur on the main thread"); |
| |
| NSFileManager *fileManager = [NSFileManager defaultManager]; |
| if (self.storeURL) { |
| [fileManager removeItemAtURL:self.storeURL error:NULL]; |
| } |
| } |
| } |
| |
| - (BOOL)resetStore:(NSError **)error |
| { |
| // Ensure we are on the main thread |
| NSAssert([[NSThread currentThread] isEqual:[NSThread mainThread]], |
| @"Resetting the store must occur on the main thread"); |
| |
| NSError *internalError; |
| if (error != NULL) { |
| *error = internalError; |
| } |
| |
| // |
| // Reset the managed object context |
| // |
| [_managedObjectContext reset]; |
| |
| // |
| // Remove all persistent stores |
| // |
| for (NSPersistentStore *store in _persistentStoreCoordinator.persistentStores) { |
| [_persistentStoreCoordinator removePersistentStore:store |
| error:&internalError]; |
| if (internalError != nil) { |
| return NO; |
| } |
| } |
| |
| // |
| // Delete the underlying SQLite store |
| // |
| if ([[NSFileManager defaultManager] fileExistsAtPath:self.storeURL.path]) { |
| [[NSFileManager defaultManager] removeItemAtURL:self.storeURL |
| error:&internalError]; |
| } |
| if (internalError != nil) { |
| return NO; |
| } |
| |
| // |
| // Rebuild store |
| // |
| [self addPersistentStoreToCoordinator:&internalError |
| attemptMigrationWithOldStore:NO |
| oldModelURL:nil]; |
| |
| if (internalError != nil) { |
| return NO; |
| } |
| |
| // |
| // Send update that the store was just reset, and listeners should purge any NSManagedObjects |
| // they are holding onto. |
| // |
| [[NSNotificationCenter defaultCenter] postNotificationName:kSMStoreControllerDidReset |
| object:self]; |
| |
| return YES; |
| } |
| |
| #pragma mark - |
| |
| - (void)setSaveOnAppStateChange:(BOOL)saveOnAppStateChange |
| { |
| _saveOnAppStateChange = saveOnAppStateChange; |
| |
| if (!saveOnAppStateChange) { |
| [[NSNotificationCenter defaultCenter] removeObserver:self |
| name:UIApplicationDidEnterBackgroundNotification |
| object:nil]; |
| |
| [[NSNotificationCenter defaultCenter] removeObserver:self |
| name:UIApplicationWillTerminateNotification |
| object:nil]; |
| } else { |
| [[NSNotificationCenter defaultCenter] addObserver:self |
| selector:@selector(saveContext) |
| name:UIApplicationDidEnterBackgroundNotification |
| object:nil]; |
| |
| [[NSNotificationCenter defaultCenter] addObserver:self |
| selector:@selector(saveContext) |
| name:UIApplicationWillTerminateNotification |
| object:nil]; |
| } |
| } |
| |
| #pragma mark - CoreData Stack |
| |
| - (void)saveContext |
| { |
| [self.managedObjectContext stm_queueBlockSaveAndWait]; |
| } |
| |
| - (NSManagedObjectContext *)managedObjectContext |
| { |
| if (_managedObjectContext) { |
| return _managedObjectContext; |
| } |
| |
| NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; |
| if (coordinator) { |
| _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; |
| [_managedObjectContext setPersistentStoreCoordinator:coordinator]; |
| } |
| |
| return _managedObjectContext; |
| } |
| |
| - (NSManagedObjectModel *)managedObjectModel |
| { |
| if (_managedObjectModel) { |
| return _managedObjectModel; |
| } |
| |
| NSAssert(_modelURL, @"ModelURL was nil! Could not find resource"); |
| |
| _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:self.modelURL]; |
| return _managedObjectModel; |
| } |
| |
| - (NSPersistentStoreCoordinator *)persistentStoreCoordinator |
| { |
| if (_persistentStoreCoordinator != nil) { |
| return _persistentStoreCoordinator; |
| } |
| |
| NSError *error = nil; |
| _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel]; |
| |
| [self addPersistentStoreToCoordinator:&error |
| attemptMigrationWithOldStore:YES |
| oldModelURL:self.oldModelURL]; |
| |
| if (error) { |
| return nil; |
| } |
| |
| return _persistentStoreCoordinator; |
| } |
| |
| |
| |
| |
| - (BOOL)addPersistentStoreToCoordinator:(NSError **)error |
| attemptMigrationWithOldStore:(BOOL)attemptMigrationWithOldStore |
| oldModelURL:(NSURL *)oldModelURL |
| { |
| if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType |
| configuration:nil |
| URL:self.storeURL |
| options:nil |
| error:error]) |
| { |
| /* |
| Replace this implementation with code to handle the error appropriately. |
| |
| |
| Typical reasons for an error here include: |
| * The persistent store is not accessible; |
| * The schema for the persistent store is incompatible with current managed object model. |
| Check the error message to determine what the actual problem was. |
| |
| |
| If the persistent store is not accessible, there is typically something wrong with the file path. |
| Often, a file URL is pointing into the application's resources directory instead of a writeable directory. |
| |
| If you encounter schema incompatibility errors during development, you can reduce their frequency by: |
| * Simply deleting the existing store: |
| [[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil] |
| |
| * Performing automatic lightweight migration by passing the following dictionary as the options parameter: |
| [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil]; |
| |
| Lightweight migration will only work for a limited set of schema changes; consult "Core Data Model Versioning and Data Migration Programming Guide" for details. |
| |
| */ |
| |
| NSLog(@"The model used to open the store is incompatable with the one used to create the store! Performing lightweight migration."); |
| |
| NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption : @(YES), |
| NSInferMappingModelAutomaticallyOption : @(YES)}; |
| NSError *migrationError; |
| if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType |
| configuration:nil |
| URL:self.storeURL |
| options:options |
| error:&migrationError]) { |
| |
| NSError *mappingError; |
| NSError *moveError = nil; |
| BOOL migrationSuccess = NO; |
| NSURL *newStoreURL = nil; |
| |
| if (attemptMigrationWithOldStore) { |
| |
| // generate a managed object model for the old schema, we will use it to attempt a migration |
| // between the old and new schemas |
| NSManagedObjectModel *oldModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:oldModelURL]; |
| |
| NSMappingModel *mappingModel = [NSMappingModel inferredMappingModelForSourceModel:oldModel |
| destinationModel:self.managedObjectModel |
| error:&mappingError]; |
| // attempt a migration if the mapping model exists |
| if (mappingModel != nil) { |
| // Create a migration manager to perform the migration. |
| NSMigrationManager *manager = [[NSMigrationManager alloc] |
| initWithSourceModel:oldModel |
| destinationModel:self.managedObjectModel]; |
| |
| NSError *migrationError = nil; |
| |
| newStoreURL = [self.storeURL URLByAppendingPathExtension:@"foo"]; |
| [[NSFileManager defaultManager] removeItemAtURL:newStoreURL error:nil]; //clear out any existing temporary file |
| |
| migrationSuccess = [manager migrateStoreFromURL:self.storeURL |
| type:NSSQLiteStoreType |
| options:nil |
| withMappingModel:mappingModel |
| toDestinationURL:newStoreURL |
| destinationType:NSSQLiteStoreType |
| destinationOptions:nil |
| error:&migrationError]; |
| |
| if (migrationSuccess) { |
| |
| // if the migration succeeds, remove the old sql store and move the new sql store to the old stores location |
| // on disk |
| [[NSFileManager defaultManager] removeItemAtURL:self.storeURL error:nil]; |
| [[NSFileManager defaultManager] moveItemAtURL:newStoreURL toURL:self.storeURL error:&moveError]; |
| } |
| } |
| } |
| |
| // if any step in the migration has failed, delete both of the stores |
| if (moveError != nil || !migrationSuccess) { |
| |
| [[NSFileManager defaultManager] removeItemAtURL:self.storeURL error:nil]; |
| [[NSFileManager defaultManager] removeItemAtURL:newStoreURL error:nil]; |
| } |
| |
| // |
| // If you still can't add a persistent store after all this, throw an error. |
| // |
| if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType |
| configuration:nil |
| URL:self.storeURL |
| options:options |
| error:error]) { |
| return NO; |
| } |
| } |
| } |
| return YES; |
| } |
| |
| @end |