Migrating Core Data to new UIManagedDocument in iOS 5
- by samerpaul
I have an app that has been on the store since iOS 3.1, so there is a large install base out there that still uses Core Data loaded up in my AppDelegate. In the most recent set of updates, I raised the minimum version to 4.3 but still kept the same way of loading the data. Recently, I decided it's time to make the minimum version 5.1 (especially with 6 around the corner), so I wanted to start using the new fancy UIManagedDocument way of using Core Data.
The issue with this though is that the old database file is still sitting in the iOS app, so there is no migrating to the new document. You have to basically subclass UIManagedDocument with a new model class, and override a couple of methods to do it for you. Here's a tutorial on what I did for my app TimeTag.
Step One: Add a new class file in Xcode and subclass "UIManagedDocument"
Go ahead and also add a method to get the managedObjectModel out of this class. It should look like:
@interface TimeTagModel : UIManagedDocument
- (NSManagedObjectModel *)managedObjectModel;
@end
Step two: Writing the methods in the implementation file (.m)
I first added a shortcut method for the applicationsDocumentDirectory, which returns the URL of the app directory.
- (NSURL *)applicationDocumentsDirectory
{
return [[[NSFileManagerdefaultManager] URLsForDirectory:NSDocumentDirectoryinDomains:NSUserDomainMask] lastObject];
}
The next step was to pull the managedObjectModel file itself (.momd file). In my project, it's called "minimalTime".
- (NSManagedObjectModel *)managedObjectModel
{
NSString *path = [[NSBundlemainBundle] pathForResource:@"minimalTime"ofType:@"momd"];
NSURL *momURL = [NSURL fileURLWithPath:path];
NSManagedObjectModel *managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:momURL];
return managedObjectModel;
}
After that, I need to check for a legacy installation and migrate it to the new UIManagedDocument file instead. This is the overridden method:
- (BOOL)configurePersistentStoreCoordinatorForURL:(NSURL *)storeURL ofType:(NSString *)fileType modelConfiguration:(NSString *)configuration storeOptions:(NSDictionary *)storeOptions error:(NSError **)error
{
// If legacy store exists, copy it to the new location
NSURL *legacyPersistentStoreURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"minimalTime.sqlite"];
NSFileManager* fileManager = [NSFileManagerdefaultManager];
if ([fileManager fileExistsAtPath:legacyPersistentStoreURL.path]) {
NSLog(@"Old db exists");
NSError* thisError = nil;
[fileManager replaceItemAtURL:storeURL withItemAtURL:legacyPersistentStoreURL backupItemName:niloptions:NSFileManagerItemReplacementUsingNewMetadataOnlyresultingItemURL:nilerror:&thisError];
}
return [superconfigurePersistentStoreCoordinatorForURL:storeURL ofType:fileType modelConfiguration:configuration storeOptions:storeOptions error:error];
}
Basically what's happening above is that it checks for the minimalTime.sqlite file inside the app's bundle on the iOS device.
If the file exists, it tells you inside the console, and then tells the fileManager to replace the storeURL (inside the method parameter) with the legacy URL. This basically gives your app access to all the existing data the user has generated (otherwise they would load into a blank app, which would be disastrous).
It returns a YES if successful (by calling it's [super] method).
Final step: Actually load this database
Due to how my app works, I actually have to load the database at launch (instead of shortly after, which would be ideal). I call a method called loadDatabase, which looks like this:
-(void)loadDatabase
{
static dispatch_once_t onceToken;
// Only do this once!
dispatch_once(&onceToken, ^{
// Get the URL
// The minimalTimeDB name is just something I call it
NSURL *url = [[selfapplicationDocumentsDirectory] URLByAppendingPathComponent:@"minimalTimeDB"];
// Init the TimeTagModel (our custom class we wrote above) with the URL
self.timeTagDB = [[TimeTagModel alloc] initWithFileURL:url];
// Setup the undo manager if it's nil
if (self.timeTagDB.undoManager == nil){
NSUndoManager *undoManager = [[NSUndoManager alloc] init];
[self.timeTagDB setUndoManager:undoManager];
}
// You have to actually check to see if it exists already (for some reason you can't just call "open it, and if it's not there, create it")
if ([[NSFileManagerdefaultManager] fileExistsAtPath:[url path]]) {
// If it does exist, try to open it, and if it doesn't open, let the user (or at least you) know!
[self.timeTagDB openWithCompletionHandler:^(BOOL success){
if (!success) {
// Handle the error.
NSLog(@"Error opening up the database");
}
else{
NSLog(@"Opened the file--it already existed");
[self refreshData];
}
}];
}
else {
// If it doesn't exist, you need to attempt to create it
[self.timeTagDBsaveToURL:url forSaveOperation:UIDocumentSaveForCreatingcompletionHandler:^(BOOL success){
if (!success) {
// Handle the error.
NSLog(@"Error opening up the database");
}
else{
NSLog(@"Created the file--it did not exist");
[self refreshData];
}
}];
}
});
}
If you're curious what refreshData looks like, it sends out a NSNotification that the database has been loaded:
-(void)refreshData {
NSNotification* refreshNotification = [NSNotificationnotificationWithName:kNotificationCenterRefreshAllDatabaseData object:self.timeTagDB.managedObjectContext userInfo:nil];
[[NSNotificationCenter defaultCenter] postNotification:refreshNotification];
}
The kNotificationCenterRefreshAllDatabaseData is just a constant I have defined elsewhere that keeps track of all the NSNotification names I use.
I pass the managedObjectContext of the newly created file so that my view controllers can have access to it, and start passing it around to one another.
The reason we do this as a Notification is because this is being run in the background, so we can't know exactly when it finishes. Make sure you design your app for this! Have some kind of loading indicator, or make sure your user can't attempt to create a record before the database actually exists, because it will crash the app.