I have a Core Data application which I plan to update with a new schema. The lightweight migration seems to work, but it takes time proportional to the amount of data in the database. This occurs in the didFinishLaunchingWithOptions
phase of the app.
I want to avoid <app> failed to launch in time
problems, so I assume I cannot keep the migration in the didFinishLaunchingWithOptions
method.
I assume the best method would involve performing the migration in a background thread. I assume also that I'd need to defer loading of the main ViewController until the loading completes to avoid using the managedObjectContext
until initialization completes.
Does this make sense, and is there example code (maybe in Apple sample projects) of this sort of initialization?
You can't put migration in a NSOperation
because it will need to run on the main thread. What you need to do is to get out of the -applicationDidFinishLaunching:
method without touching the Core Data stack. If you can finish that method (and that run loop cycle) quickly then your app will not be terminated and you can take as long as the user will put up with to finish your migration.
See my answer here: How to switch from Core Data automatic lightweight migration to manual?
To clarify my position on this. It is inherently possibly to do just about anything. However, doing a migration on a background thread is a bad idea. It is extremely difficult to guarantee that the stack will never get touched during the migration as well as a whole host of other threading specific complications.
It is possible to do it but it involves a high degree of risk that is completely unnecessary. The main thread can and should be used to do a migration of the main Core Data stack. It is easy to put up a modal dialog to let the user know a migration is occurring and then perform the migration on the main thread.
If you are in a situation where your migrations are taking a signficant amount of time then it is also highly recommended that you switch from automatic migration to manual migration with a mapping model so that you can:
A lot has changed since this was originally answered.
The answer now for migrations is to fire them on a background queue (via dispatch_async
of the NSPersistentStoreCoordinator
's addStore...
call).
This also means that you need to make sure your UI can handle the persistence layer being empty/not available for an unknown period of time. How to do that depends on your application.
For example, you can have a temporary UI that shows dancing cats while the persistence layer does the migration. The user experience is up to you.
However, you do NOT want to let the user create data while the migration is happening. That will make it difficult to merge later (if at all).
Sorry Marcus, I have to respectfully disagree. You can migrate in the background.
My migriation runs on a background thread. It can take over 10 seconds on slow devices, so I start it on a background thread and have a specific modal view controller to show progress.
The way to do this is split your normal loading sequence into two phases.
Phase 1) Do everything you would normally do at launch that doesn't require managed objects. The end of this phase is defined by a check to determine if migration is required.
Phase 2) Do everything that normally happens at launch that requires managed objects. This phase is an immediate continuation of Phase 1) when no migration was required.
This way, your app finishes launching regardless of the duration of migration processing.
To perform a long migration successfully, I use a modal view controller showing feedback of the migration progress to the user. I then commence the migration in the background thread, while the modal view controller uses KVO to update it's progress bar.
At the end of the migration, it closes down the whole core data "stack", and a callback to the main thread will dismiss the modal and continue on to Phase 2).
This whole process works flawlessly, although I still have an open question on a way to have the automatic lightweight migration reveal it's progress like the manual migration does.
Here is an outline of an approach similar to what ohhorob describes. The difference is that I just display an animated "Upgrading..." HUD rather than a more informative progress bar.
The key point is that nothing tries to access Core Data until the migration has had a chance to do its thing. Therefore, I move all the remaining launch code (including the setting of the rootViewController) that could potentially touch Core Data into a separate method. I then call this method after the migration has completed, or immediately if no migration is required. That looks like this:
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
[self.window makeKeyAndVisible];
if ([MyManagedObject doesRequireMigration]) { // -doesRequireMigration is a method in my RHManagedObject Core Data framework
[SVProgressHUD showWithStatus:NSLocalizedString(@"Upgrading...", nil) maskType:SVProgressHUDMaskTypeGradient];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// do your migration here
dispatch_async(dispatch_get_main_queue(), ^{
[SVProgressHUD showSuccessWithStatus:NSLocalizedString(@"Success!",nil)];
[self postApplication:application didFinishLaunchingWithOptions:launchOptions];
});
});
} else {
[self postApplication:application didFinishLaunchingWithOptions:launchOptions];
}
return YES;
}
-(void)postApplication:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window.rootViewController = ....
// anything else you want to run at launch
}
This approach should play nice with the watchdog that monitors startup times since the migration will take place off the main thread.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With