Migrating a Core Data store to an App Group shared container
Runway handles the release coordination and busywork so you can focus on building great apps. You do the building, we'll do the shipping.
If you have a Core Data store set up for your iOS app in its default location, you must know that widgets won’t be able to access it.
The reason for this is that widgets run in a separate process, and they can’t access the app’s directories, where Core Data stores are created by default.
Thankfully, there is a way to set up a shared location where we can move the store to and that both the app and the widget can access by using an App Group.
Creating an app group
Creating an app group is straightforward and can be done directly in Xcode. To do so, go to your target’s Signing & Capabilities
section and press on the + Capability
button.
Then, search for App Groups
and press on double-click on the result to add it to your target.
Finally, enter a name and create the App Group. If everything has gone well, you should see the App Group in the list of capabilities for the target:
Note that container IDs must be prefixed with
group.
and then be followed by a custom string in reverse DNS notation. If you’d like to find out more, please check out Apple’s documentation on the topic.
Creating a store in the App Group folder
Now that you have created an App Group, you can create a store in the group’s shared container with just a few lines of code:
final class CoreDataManager {
let container: NSPersistentContainer
init?(inMemory: Bool = false) {
let storageName = "MigrationDemo"
let container = NSPersistentContainer(name: storageName)
// 1
guard let storeLocation = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.dev.polpiella.MigrationDemo")?.appendingPathComponent("\(storageName).sqlite") else {
return nil
}
// 2
let description = NSPersistentStoreDescription(url: storeLocation)
// 3
container.persistentStoreDescriptions = [description]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
print("Unresolved error \(error), \(error.userInfo)")
}
})
self.container = container
}
}
The code above does the following to create a store in the App Group:
- Retrieve the URL of the app group folder by using the
FileManager
’scontainerURL(forSecurityApplicationGroupIdentifier:)
method. - Create an
NSPersistentStoreDescription
object and give it the App Group folder URL you have just retrieved. This will override the default store location and will allow the widgets to access persistent data. - Set the
NSPersistentStoreDescription
you have just created in the container.
I would like to give credit to this awesome video by Flo Writes Code that helped me figure out what to do here.
Migrating a current store to the app group folder
In the previous section I showed you how you can create a Core Data store in a shared App Group container from scratch but, let’s say you already have a store in a different location and you replace your code with the one above, what will happen?
The answer is that you will lose all your data as a new store will be created in a different location and no migration will happen out of the box.
Thankfully, there is a way to migrate the data from the current store to new store but it requires a bit more code:
final class CoreDataManager {
let container: NSPersistentContainer
init(inMemory: Bool = false) {
let storageName = "MigrationDemo"
let container = NSPersistentContainer(name: storageName)
let fileManager = FileManager.default
// 1
guard let sharedStoreLocation = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.dev.polpiella.MigrationDemo")?.appendingPathComponent("\(storageName).sqlite"),
let currentStoreLocation = container.persistentStoreDescriptions.first?.url else {
fatalError("Expected both locations to exist...")
}
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
} else {
// 2
if fileManager.fileExists(atPath: currentStoreLocation.path) && !fileManager.fileExists(atPath: sharedStoreLocation.path) {
let coordinator = container.persistentStoreCoordinator
do {
// 3
try coordinator.replacePersistentStore(at: sharedStoreLocation, destinationOptions: nil, withPersistentStoreFrom: currentStoreLocation, sourceOptions: nil, type: .sqlite)
// 4
try? coordinator.destroyPersistentStore(at: sharedStoreLocation, type: .sqlite, options: nil)
} catch {
print("\(error.localizedDescription)")
}
} else {
// 5
let description = NSPersistentStoreDescription(url: sharedStoreLocation)
container.persistentStoreDescriptions = [description]
}
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
print("Unresolved error \(error), \(error.userInfo)")
}
})
self.container = container
}
}
Note that I was able to get to this solution thanks to this brilliant answer in Stack Overflow on how to migrate a store and this other equally as brilliant answer pointing out that files are not deleted when using the
destroyPersistentStore
method.
The extra code does the following to achieve the migration:
- Get the URLs for both the current Core Data store and the one in the shared container.
- Use
FileManager
to check if the current store exists at the default location and if the store in the App Group folder doesn’t exist. If this is the case, a migration needs to happen to move the data from the current store to the one in the shared container. By default and if not specified otherwise, the store will always be created by CoreData in the Application Support directory. - Call the
replacePersistentStore
method on thepersistentStoreCoordinator
to replace the current store with the one in the App Group folder. - Call the
destroyPersistentStore
method on thepersistentStoreCoordinator
to delete the store that was created in the Application Support directory. Contrary to what you might think, this method does not delete the underlying database files so you will need to do this manually. - If the new App Group store exists, it means that a migration isn’t needed and the
NSPersistentStoreDescription
object with the App Group URL can be set up. This step is crucial as otherwise Core Data will create a new store in the Application Support directory.
Next time you run the app you will see that all your existing data has been migrated correctly to the App Group shared container and that your widgets can access it! 🎉
💡 Pro tip: If you’d like to make sure that the migration has happened and the old store is empty, you can open the
.sqlite
database files in a tool like Core Data Lab by Betamagic.