How to Perform a Lightweight Core Data Migration

Scott Gardner
Cheers to Lightweight Migrations

Cheers to Lightweight Migrations

All iOS apps deal with data, and many apps need to save data locally. Core Data is a compelling choice for data management and persistence in iOS and OS X, which if you’re here you probably already know! :]

But what if you want to change the data model for your app after you ship? To do this, you’ll need to create something called a migration, which is a fancy way of telling Xcode how to transition the data from the old model to the new model.

There are three primary ways to create a migration: automatic (aka lightweight), manual, and custom code. In reality, the migration process may involve one or more of these techniques.

The golden rule when it comes to Core Data migrations is, choose lightweight whenever possible. Manual migrations and migrations requiring custom code are a magnitude more complex and memory intensive.

Thanks to the über smart engineers on the Core Data team at Apple, you can use lightweight migrations for an increasing percentage of your migration needs with every new release of iOS and OS X. And even when a more complex migration is necessary, you can mix in lightweight migrations to do some of the heavy lifting before you have to dig in.

In this tutorial, you will learn how to successfully perform a lightweight Core Data migration.

The app you will create in this tutorial will enable users to record passing thoughts. You know, those moments of inspiration and brilliance that happen where else but when you’re standing in line or sitting on the throne? Soon, there will be an App for that! :]

Note: This tutorial assumes you are familiar with the basics of using Core Data. If you are new to Core Data, no problem – we’ve got you covered with some great Core Data tutorials including this and this and this.

Getting Started

You will be building this app from scratch, utilizing one of Xcode’s built in templates. Create a new project in Xcode (File\New\Project…). Select Master-Detail Application and click Next.

Select Master-Detail Application

Enter PassingThoughts for the Product Name, enter an Organization Name, enter a Company Identifier prefix such as com.mycompany, select iPhone for Devices, and checkmark Use Storyboards, Use Core Data, and Use Automatic Reference Counting.

Enter options for your new project

Note: I will not be covering source control in this tutorial. However, I highly recommend using source control, or at least create snapshots (File\Create Snapshot…), and commit/create after every significant iteration. Especially when working with Core Data migrations, you can never have too many backups!

The last thing you want to have happen is, you are going along, happily making changes to the data model, you build and run, and BAM! Exception error: “The model used to open the store is incompatible with the one used to create the store,” or “Can’t find model for source store,” or other such terrifying message. And you don’t have a very recent, known-good, commit or snapshot to restore.

Not familiar with Git? To learn more about using Git source control with Xcode, check out this tutorial.

Choose a suitable location to save your project and click Create.

As far as habits go, here’s a good one: build and run early, build and run often. So let’s do that now. I’ll be using the iPhone 6.0 Simulator in this tutorial, but you can also use a provisioned iPhone or iPod touch device instead if you want (check out this tutorial to learn about device provisioning).

Click Run (or Product\Run), and you should see the standard Master-Detail app in all its glory.

Important: Tap the + button a few times to create some entries – you’ll need some sample data to upgrade with migrations in this tutorial!

Master-Detail app

Core Data Cliff Notes

Before you continue on, let’s take a quick review of the basics of Core Data.

The Core Data model is made up of two components:

  • The managed object model: The managed object model describes the database schema (entities and their properties).
  • The Core Data stack: The Core Data stack is made up of three parts (you can have more than one set of these):
    • Persistent store: Think of this as the database itself (usually SQLite).
    • Persistent store coordinator: Think of this as the “database connection”. Each coordinator can have one and only one managed object model and one persistent store (which it is initialized with).
    • Managed object context: Think of this as the “scratch pad.” Each context can have one and only one persistent store coordinator (which it is instantiated with).

Check out this diagram to see what I mean:

Core Data model

Got the terminology down? OK, let’s talk migrations!

Hello Migration

When the model does not match the store, a migration is required. In order to perform a migration, Core Data (technically, an instance of NSMigrationManager) requires these things:

  • The destination managed object model (the one with the changes)
  • A managed object model that can open the existing store
  • The ability to infer mapping between the two models (for a lightweight migration), or a manual mapping model
  • Permission to attempt to perform an automatic migration (for a lightweight migration)

It is therefore absolutely essential that you never make changes to the managed object model for a released version of an app. That is, if your app is already in the App Store, don’t change a single thing in that version of the managed object model.

But obviously you need to change the database model over time – so what is a poor developer to do?!

I hinted at the answer… create a new version of the managed object model! This reminds me to mention some other best practices to adopt when working with Core Data:

  • Create a new model version for every release version of an app
  • Keep a copy of every release version of an app (you’re already doing this, right?)
  • Keep a copy of every release version backing SQLite store containing suitable test data

Note: Core Data does not perform migrations linearly. It will attempt to perform a migration from whatever the current version is on a user’s device to the newest version available when the user selects to install the latest update in the App Store.

For example, let’s say you have an app in the App Store and you have already released a second version of the app with changes to the model. Some of your users install the update. Others do not.

Then you create a 3rd version of the app/model and release it to the App Store. Your existing users may have the 1st or 2nd version of your app on their device. Core Data will attempt to migrate from whatever version the user currently has on their device directly to version 3 of your app/model, e.g., from version 1 to 3, or from version 2 to 3.

Along these lines, here’s a good guide to follow when you need to make changes to your managed object model, to help ensure a smooth migration process (you’ll be going through these steps shortly):

  • Create a new version of your managed object model and switch to that model
  • Make changes to that new model, being mindful of what kind of migration(s) your changes will require
  • Test every combination of possible migration, e.g., from version 1 to 2, from 1 to 3, and from 2 to 3

Imagine how complex this could get once you’ve released a dozen or more versions! Lightweight migrations will quickly become your BFF, so long as your changes do not require a more complex migration.

Let’s Recap

So, to sum it all up, here’s what it takes for a migration to work:

  • When the current version of the model (i.e., the new one with changes) does not match the persistent store coordinator…
  • …and Core Data finds a model that can open the existing (old) store on the user’s device…
  • …and it finds either a mapping model or the permission and ability to infer mapping (for a lightweight migration)…
  • …and it has permission to attempt to automatically perform a migration (for a lightweight migration)…
  • …a migration is kicked off, w00t! :D

And if not, well, Houston, we have a problem ;]

What Lightweight Migrations Can Do For You

I could rule the world with lightweight migrations!

I could rule the world with lightweight migrations!

Remember, you want to use lightweight migrations whenever possible because they are the simplest (and least error prone) option.

Lightweight migrations can handle the following changes:

  • Adding or removing an entity, attribute, or relationship
  • Making an attribute non-optional with a default value
  • Making a non-optional attribute optional
  • Renaming an entity or attribute using a renaming identifier

A more complex migration will be necessary for any other changes, or if lightweight migration is not enabled (i.e., Core Data is not given permission to infer mapping and perform an automatic migration).

On a related subject, there are some changes that do not require a migration, basically anything that doesn’t change the underlying SQLite backing store, including:

  • Changing the name of an NSManagedObject subclass
  • Adding or removing a transient property
  • Making changes to the user info dictionary
  • Changing validation rules

To enable lightweight migrations, you need to pass a dictionary containing two keys to the options parameter of the method that initializes the persistent store coordinator. These keys are:

  • NSMigratePersistentStoresAutomaticallyOption – attempt to automatically migrate versioned stores
  • NSInferMappingModelAutomaticallyOption – attempt to create the mapping model automatically

Preparing For A Model Update: The Challenge!

In this tutorial, you’re going to update the storyboard and associated view controllers, and then modify the managed object model.

But before you do that, you have to prepare your project in order to handle modifications to your managed object model. Based on what you’ve learned so far, see if you can figure out how to do this on your own!

Important note: Don’t run your project after making these modifications yet. This is because if you do it will upgrade your model to the new version, and you don’t want to do that until you actually modify it :]

If you get stuck, just click the Show buttons below!

Solution Inside: Click for Hint SelectShow
Solution Inside: Click for Solution SelectShow

Congratulations if you knew the answer before showing the solution! :] Be sure to perform the steps in the solution before proceeding.


Open PassingThoughts 2.xcdatamodel (double-check to make sure you have selected the correct one), double-click on the Event entity under ENTITIES in the Document Outline, and rename it to Thought. With the Thought entity still selected, in the Data Model inspector (View\Utilities\Show Data Model Inspector), in the Versioning Section, enter Event for Renaming ID.

What you just did is let Xcode know that when it upgrades the database, it should move all data from the Event table to the Thought table. Pretty easy, eh?

Next, click + in the Attributes section to create two additional attributes:

  • what – Type: string
  • where – Type: string

Double-click the timeStamp attribute in the Attributes section in the Editor and rename it when. With the when attribute still selected, in the Data Model inspector, in the Versioning Section, enter timeStamp for Renaming ID.

Your managed object model should look like this:

Core Data model

That’s it for the changes to the model – now let’s fix up the views to display this!


Open up MainStoryboard.storyboard in Xcode, select the Master View Controller Scene, double-click on the word Master in the navigation bar and change the text to “Thoughts.”

Add a “+” bar button to the navigation bar on the right by dragging a UIBarButtonItem from the Object library (View\Utilities\Show Object Library) to the right of the word “Thoughts.” Select the bar button and in the Attributes inspector (View\Utilities\Show Attributes Inspector) in the Bar Button Item section, change Identifier to Add.

Add plus bar button to Master View Controller scene

Control-drag from the “+” bar button to the Detail View Controller scene and release. Select push in the popup.

Create segue

There will now be two segues from the Master View Controller scene to the Detail View Controller scene. Select the top segue, which should be the new one, and in the Attributes inspector (View\Utilities\Show Attributes Inspector), in the Storyboard Segue section, enter AddThought for Identifier. Note that Identifier should be blank at first. If it’s not (i.e., it is showDetail), select the other segue.

Name segue AddThought

Select the other segue and rename it ShowThought.

Note: It would be a better user experience, and more inline with Apple’s Human Interface Guidelines, to present an “add” view via a modal segue and include a Cancel button. When using Core Data, I would instantiate an undo manager with the managed object context, and then call rollback on the context in a delegate method when the user taps Cancel. However, I will not be covering how to implement this, in order to keep things focused on the tutorial topic.

Select the prototype cell, and in the Attributes inspector (View\Utilities\Show Attributes Inspector), in the Table View Cell section, change Style to Subtitle.

Change prototype cell style to Subtitle

Select the Detail View Controller scene, delete the UILabel with the placeholder text “Detail view content goes here,” and add the following objects by dragging them from the Object library (View\Utilities\Show Object Library) with the specified additional attributes (View\Utilities\Show Attributes Inspector) in the Label section) and size dimensions (View\Utilities\Show Size Inspector) in the View section:

  • UILabel – Font: System Bold, Tighten Letter Spacing: checked, Text: When, Width: 60
  • UILabel – Font: System Bold, Text: Where, Width: 60
  • UILabel – Font: System Bold, Text: When, Width: 60
  • UILabel – (delete text), Width: 140
  • UITextField – Width: 140
  • UITextView – Width: 280, Height: 120

Note: I have Show Bounds Rectangles toggle on (Editor\Canvas\Show Bounds Rectangles) in order to be able to see empty objects such as the blank label and text view.

Arrange the layout of the Detail View Controller scene to look the this:

Detail View Controller scene layout

Control-drag from the UITextField to the Detail View Controller yellow proxy icon in the scene dock and select delegate in the popup.

Connect text field to delegate

Control-drag from the UITextView to the Detail View Controller yellow proxy icon in the scene dock and select delegate in the popup.

Connect text view to delegate

Now let’s hook these new views up to code!


Open MasterViewController.m and in the viewDidLoad method, delete the following two lines of code:

UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(insertNewObject:)];
self.navigationItem.rightBarButtonItem = addButton;

Change the signature of the insertNewObject: method to the following:

- (NSManagedObject *)insertNewObject

And add the following line to the bottom of the method:

return newManagedObject;

In the insertNewObject method, change [newManagedObject setValue:[NSDate date] forKey:@”timeStamp”]; to:

[newManagedObject setValue:[NSDate date] forKey:@"when"];

Change the prepareForSegue:sender: method to look like this:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
    // 1
    if ([[segue identifier] isEqualToString:@"AddThought"]) {
        NSManagedObject *object = [self insertNewObject];
        [[segue destinationViewController] setDetailItem:object];
        // 2
    } else if ([[segue identifier] isEqualToString:@"ShowThought"]) {
        NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
        NSManagedObject *object = [[self fetchedResultsController] objectAtIndexPath:indexPath];
        [[segue destinationViewController] setDetailItem:object];

You’re handling two cases here:

  1. Create a new thought when the user taps “+.”
  2. Change the name of the segue to ShowThought to match what you renamed it to in the storyboard.

Change the fetchedResultsController method to look like this:

- (NSFetchedResultsController *)fetchedResultsController
    if (_fetchedResultsController != nil) {
        return _fetchedResultsController;
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    // 1
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Thought" inManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:entity];
    // Set the batch size to a suitable number.
    [fetchRequest setFetchBatchSize:20];
    // Edit the sort key as appropriate.
    // 2
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"when" ascending:NO];
    NSArray *sortDescriptors = @[sortDescriptor];
    [fetchRequest setSortDescriptors:sortDescriptors];
    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    // 3
    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:@"AllThoughts"];
    aFetchedResultsController.delegate = self;
    self.fetchedResultsController = aFetchedResultsController;
	NSError *error = nil;
	if (![self.fetchedResultsController performFetch:&error]) {
	     // Replace this implementation with code to handle the error appropriately.
	     // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 
	    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    return _fetchedResultsController;

This does the following:

  1. Change the entityForName parameter to what you renamed the entity to.
  2. Change the initWithKey paramenter to what you renamed the timeStamp attribute to.
  3. Change the cacheName parameter to AllThoughts to more accurately describe this cache.

Next, change the configureCell:atIndexPath: method to look like this:

- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
    NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
    // 1
    NSDate *date = [object valueForKey:@"when"];
    cell.textLabel.text = [NSDateFormatter localizedStringFromDate:date dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterShortStyle];

    // 2
    NSString *where = [object valueForKey:@"where"];
    NSString *what = [object valueForKey:@"what"];
    if ([where length]) {
        cell.detailTextLabel.text = [NSString stringWithFormat:@"Where: %@", where];
    } else if ([what length]) {
        cell.detailTextLabel.text = [NSString stringWithFormat:@"What: %@", what];
    } else {
        cell.detailTextLabel.text = nil;

This does the following:

  1. Set the textLabel to a localized formatted date.
  2. Set the detailTextLabel to either where or what or nil.

Next you are going to use a pretty cool feature in Xcode (as of version 4) that will allow you to declare IBOutlet properties in the class file by Control-dragging from the object in the storyboard scene to the class file.

With MainStoryboard.storyboard open in Xcode, select the Detail View Controller scene, open the Assistant editor (View\Assistant Editor\Show Assistant Editor), make sure that Automatic is displayed in the ribbon menu above the right editor (or click on whatever is displayed there and select Automatic), and make sure that the MasterViewController.h interface file is displayed in the right editor (Navigate\Jump to Next Counterpart to toggle between interface and implementation files).

Let’s also close the Document Outline by clicking anywhere in the storyboard editor and then select Editor\Hide Document Outline from the menu. Your Xcode workspace should look similar to this (by the way, you can click on any image to open it in a larger view):

Xcode workspace in Assistant Editor mode

Note: I like to organize my project files into Models, Views, and Controllers groups. Core Data models, NSManagedObject subclasses, and any other model files go in the Models group. Storyboards and custom view class files (such as for a custom table view cell) go in the Views group, and all controller class files go in the Controllers group. While it may not be worth the effort on a project this small, in larger projects I have found keeping a neat workspace is pure zen. Just thought I’d share this with you. Now back to our regularly scheduled show!

In DetailViewController.h, delete the following orphaned property declaration (this was for the “Detail view content goes here” UILabel that you deleted earlier):

@property (weak, nonatomic) IBOutlet UILabel *detailDescriptionLabel;

Switch to DetailViewController.m (Navigate\Jump to Next Counterpart), and declare adopting the UITextFieldDelegate and UITextViewDelegate protocols by changing the line @interface DetailViewController () to look like this:

@interface DetailViewController () <UITextFieldDelegate, UITextViewDelegate>

Change the configureView method to look like this:

- (void)configureView
    // Update the user interface for the detail item.

    if (self.detailItem) {
        // 1
        NSDate *date = [self.detailItem valueForKey:@"when"];
        self.whenLabel.text = [NSDateFormatter localizedStringFromDate:date dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterShortStyle];
        // 2
        self.whereTextField.text = [self.detailItem valueForKey:@"where"];
        self.whatTextView.text = [self.detailItem valueForKey:@"what"];

You’ll get some errors when you add in this code, but don’t worry – you’ll fix them up soon. Here you’ve done the following:

  1. Set the whenLabel to a localized formatted date.
  2. Set whereTextField and whatTextView text to where and what, respectively.

Add the following code after the didReceiveMemoryWarning method:

- (void)viewWillDisappear:(BOOL)animated
    [super viewWillDisappear:animated];
    [self saveContext];

When the user taps the Thoughts (back) button, the saveContext method will be called.

In the storyboard in the left editor, Control-drag from the empty UILabel (to the right of the “When” UILabel) to the extension in DetailViewController.m in the right editor, so that your pointer is right between @interface DetailViewController () and – (void)configureView; (a blue line should separate those two lines of code) and release the click. In the pop-up that appears, enter whenLabel for Name and click Connect.

Create when label property

The class extension should now look like this:

@interface DetailViewController ()
@property (weak, nonatomic) IBOutlet UILabel *whenLabel;
- (void)configureView;

Note: Should you ever make a mistake or change your mind about a “connected” IBOutlet or IBAction (the circle to the left of the property or method in the line-number gutter will be filled in), before deleting the code in the class file or connecting to a different outlet or action, select the item in the storyboard scene (or xib) and go to the Connections inspector (View\Utilities\Show Connections Inspector) and break the connection by clicking the little “x” button.

If you delete an object in a storyboard scene (or xib), the little circle for any previously connected outlets or actions will be hollow, indicating it is no longer connected to anything (“orphaned,” as I called it earlier with the placeholder UILabel from the template that you deleted).

Control-drag from the empty UITextField (to the right of the “Where” UILabel) to the extension in DetailViewController.m in the right editor, right below the whenLabel property declaration you just made and release. In the pop-up that appears, enter whereTextField for Name and click Connect. The class extension should now look like this:

@interface DetailViewController ()
@property (weak, nonatomic) IBOutlet UILabel *whenLabel;
@property (weak, nonatomic) IBOutlet UITextField *whereTextField;

Control-drag from the empty UITextView (below the “What” UILabel) to the extension in DetailViewController.m in the right editor, right below the whereTextField property declaration you just made and release. In the pop-up that appears, enter whatTextView for Name and click Connect. The class extension should now look like this:

@interface DetailViewController ()
@property (weak, nonatomic) IBOutlet UILabel *whenLabel;
@property (weak, nonatomic) IBOutlet UITextField *whereTextField;
@property (weak, nonatomic) IBOutlet UITextView *whatTextView;

Enter the following code in viewDidLoad, right below [self configureView];:

    // 1
    [self.whatTextView becomeFirstResponder];
    // 2
    UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hideKeyboard)];
    tapGestureRecognizer.cancelsTouchesInView = NO;
    [self.view addGestureRecognizer:tapGestureRecognizer];

This does the following:

  1. Place the insertion cursor in the what text view and display the keyboard.
  2. Add a tap gesture recognizer to the view, so that if the keyboard is displayed, tapping anywhere outside of an input object will call the hideKeyboard method to dismiss the keyboard.

Enter the following code at the bottom of the file, right above @end:

#pragma mark - Private methods

- (void)hideKeyboard
    [self.view endEditing:YES];

Enter the following code after the hideKeyboard method:

- (void)saveContext
    NSError *error = nil;
    NSManagedObjectContext *managedObjectContext = [self.detailItem managedObjectContext];
    if (managedObjectContext != nil) {
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);

This method will save the managed object context, which essentially saves any changes in the database.

Finally, add this last bit of code:

#pragma mark - UITextFieldDelegate

// 1
- (BOOL)textFieldShouldReturn:(UITextField *)textField
    [textField resignFirstResponder];
    return YES;

// 2
- (void)textFieldDidEndEditing:(UITextField *)textField
    if ([textField isEqual:self.whereTextField]) {
        [self.detailItem setValue:textField.text forKey:@"where"];

#pragma mark - UITextViewDelegate

// 3
- (void)textViewDidEndEditing:(UITextView *)textView
    if ([textView isEqual:self.whatTextView]) {
        [self.detailItem setValue:textView.text forKey:@"what"];

These methods do the following:

  1. Hide the keyboard when return is tapped.
  2. Set the detailItem’s where attribute to the contents of the whereTextField.
  3. Set the detailItem’s what attribute to the contents of the whatTextView.

Note: I would normally create NSManagedObject subclasses for all entities, which would enable using dot notation for getter and setter accessors (vs. using KVC setValue:forKey:). I’ve omitted doing so here in order to keep things focused on the tutorial topic. Check out this tutorial to learn more about NSManagedObject subclasses.

And that’s it – build and run, and your project should work fine even though you upgraded the database!

Congratulations! You just successfully performed a lightweight Core Data migration. It’s nice and easy, and quite powerful! :]

Where To Go From Here?

I hope you enjoyed this tutorial and found it helpful. You can download a copy of this tutorial’s project here.

Let me know if you’d like to see a follow-up tutorial on making a more complex migration.

In the meantime, if you have any questions or comments, please join the forum discussion below!

Scott Gardner

Scott Gardner

Scott has been developing iOS apps since 2010, using Swift since the day it was announced, and RxSwift since before version 1. He's authored several video courses, tutorials, and articles on iOS app development, presented at numerous conferences, meetups, and online events, and most recently co-authored RxSwift - Reactive Programming with Swift. Scott is a veteran of the United States Marine Corps, and he lives in the midwest U.S. with his wife and daughter. You can reach Scott on LinkedIn or Twitter, or by email, and check out his open-source work on GitHub.

Other Items of Interest

Big Book SaleAll iOS 11 books on sale for a limited time! Weekly

Sign up to receive the latest tutorials from each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 19 total!

iOS Team

... 71 total!

Android Team

... 17 total!

Unity Team

... 11 total!

Articles Team

... 15 total!

Resident Authors Team

... 18 total!

Podcast Team

... 7 total!

Recruitment Team

... 9 total!