When it comes to Core Data, many application developers don’t even think about migration between versions. Even for those who do consider it, most testing usually ends up limited to a few simplistic scenarios: It’s tedious to repeatedly set up a single known state for a data store, and working against a full matrix of viable scenarios is just onerous.
Fortunately, there is functionality within Xcode to make it easier to manage and maintain a number of scenarios and restore them readily. This functionality will allow you to test your migration code from any past data model version to the current version, debug custom migrations, even profile migrations in Instruments.
Unfortunately, it’s not very well documented and, while you can preload to a device or simulator, the data package must first be created on a device.
Update 2015-04-20: This worked great in Xcode 5. Unfortunately Xcode 6 (through 6.3 as of this writing) does not behave as described for the Simulator (it does load a device just fine). Versions <6.3 will tell you “The application data package will be installed the next time you run your app in the Simulator”—every time. Version 6.3 will just silently fail to load the package. I have reported this as rdar://20622011 / Open Radar
Your Friend the .xcappdata Package
You’ve probably encountered an .xcappdata
package before, although you most likely think of it as a one-way data extraction method. Let’s walk through this with as simple a demo application as possible:
- Create a new project. As much as I despise Apple’s template Core Data stack, it will suffice for this example, so create a new “Master-Detail Application” that uses Core Data.
- Run the application on a device.
Add 5 items.
- You may stop running the application on your device—the following steps aren’t affected by it running or not.
Open the Xcode Organizer, select the “Applications” section of your device under the Devices tab.
- Select your test application. You should see a bunch of folders and files populate the “Data files in Sandbox”. (This may not populate right away, so be patient.)
- Click the “Download” button at the bottom and save the xcappdata package to your Desktop, giving it a descriptive name like
v0 - 5 items.xcappdata
. - Click “Delete” to remove the application from the device—we will try to start with a clean install for the next steps.
Here comes the magic:
- Create a new Group in your project. I name mine “Test Data”.
- Add
v0 - 5 items.xcappdata
to the group.- Select “Copy items into group folder”.
- Do not add it to any targets.
Launch the app while holding the Option key—either Option-clicking the Run button, or Option-Command-R. A scheme configuration sheet will appear.
- In the Options tab, select
v0 - 5 items.xcappdata
from the Application Data menu and click “Run”.
Voilá, your app has launched a clean install, but with five items in the database—Xcode has pre-configured your application with the data files from your xcappdata package.
Exploring the .xcappdata Package
In the Finder, Control-click the .xcappdata
package and select “Show Package Contents” from the contextual menu. Inside you will find an AppDataInfo.plist
file and an AppData
folder. Inside the AppData
folder are the Documents
, Library
, and tmp
folders that were downloaded from the device.
Any changes you make to these folders will be reflected the next time you launch the app with this scheme loading the application data. Let’s make a “clean launch” data set:
Duplicate
v0 - 5 items.xcappdata
in the Finder and rename it toClean Launch.xcappdata
.- Open Terminal and Command-drag your
Clean Launch.xcappdata
onto it to make the package’s path your current directory. - Execute
rm AppData/Documents/*sqlite*
to remove the Core Data store and supporting files. - Execute
rm -rf AppData/Library/Caches/*/.CoreDataCaches
. This clears out the invisible directory containing all yourNSFetchedResultsController
caches. If these are out of sync with the data store, you will encounter crashes and other frustrating behavior. - Add
Clean Launch.xcappdata
to your project, again remembering not to add it to any targets.
Now you have a way to start from scratch whenever you want without having to delete the app first.
Testing Implications
Every public release version of your app should be the time to freeze the version of your Core Data model. When you release the next version of your app, you will need to account for migrating the data your users currently have to the new version’s data model. (There may not actually be any version-to-version changes, but you should assume there will be.)
So as part of every release you should take a few xcappdata snapshots:
- Empty database. This is not the Clean Launch, this is the default database Core Data would generate, including anything your app automatically constructs. This is the state your app would be in if a user launched it once, didn’t do anything, and (horrors!) forgot about your app until launching an updated version.
- “Typical” user database. Run the app for a while as a typical usage scenario, then download the data package. You may have multiple “typical” users—make snapshots for all you can define.
- “Extreme” user database. This is your chance to stress test; this is your chance to ensure that your migrations can satisfy even the most outrageous user of your application. If you need to, create a command-line tool to create a large data store using the model then copy that into the package.
Add these to your project so they’re available to use. Give them descriptive names that include at least the revision (“v1”, “v2”) and scenario (“base”, “userA”, “extreme”). Adding them to project groups will help organization, but the scheme menu only lists the package name so make it identifiable.
At some point during the development of your next release, any model updates will have settled down. Use these data sets to verify that:
- Migration of old versions happens
- Migration happens correctly
- Migration happens quickly
The first two points can be tested in the simulator. If you need to write custom migration code, you will automatically start from the same point every time you launch the app after making code changes.
The last point of verification, though, needs to be run on real devices so you know that users will successfully migrate without having the launch watchdog kill the app launch and launch again.
Keep in mind that successfully migrating in the allotted launch time isn’t enough. If the app is to all intents and purposes locked during that time, the user may choose to be the Final Watchdog and terminate your app with extreme prejudice. This is your chance to ensure a positive user experience.
Manually Updating the Package
You can manually update the files in an xcappdata package to create a new snapshot, but beware of a few things:
- “Modern” SQLite adds
.sqlite-shm
and.sqlite-wal
files next to the.sqlite
file for write-ahead logging. Be sure to keep all these files in sync with a changed.sqlite
file by using an editor that understands these files (I recommend Base, I’m sure there are others), or copying/replacing these auxilliary files if you replace one.sqlite
file with another. NSFetchedResultsController
caches need to stay in sync with data store changes, too. Delete the.CoreDataCaches
directory from the package, run this package on a device long enough to exercise all theNSFetchedResultsControllers
, then download that package as the one to keep.- Your app is probably more complex than the example shown. Ensure that documents stored external to the SQLite files are where they are expected, and in sync with any other changes.
- If you can get a data file from a user or tester who is experiencing problems, that is golden. Migrate that data file into its own package and name it something like
v3-bug16002
to enshrine it in your migration testing history. Not only will this help you debug it, you will have it available going forward to avoid regressions.
Notes and Tips
- The demo app saves the context every time an item is added. If your real code does not do that, be sure
-save:
is invoked at some point before you try downloading the xcappdata or you will not have the data you thought you had. - Application Data settings persist with the scheme. I find it useful to create a new scheme named “Migration Testing” and only test migrations with that scheme. This isn’t necessary, but it’s good to know that when you’re using the app’s self-titled scheme, you aren’t accidentally resetting the data every launch.
- Git for one doesn’t track empty directories. In my experience it isn’t worth worrying about because the app’s sandbox gets laid out correctly even without directories like
/tmp
in the xcappdata package. - Be sure you don’t add the packages to the app targets. This just copies them into the app bundle as resources and bloats the app.
Radars
- rdar://17495757 – iOS NSFetchedResultsController: Stop hiding caches directory
- rdar://17539241 – Xcode: Allow creation of .xcappdata packages from Simulator