Migrating a Core Data store to an App Group shared container

Go from confusion to confidence with a step-by-step Swift Concurrency course, helping you smoothly migrate to Swift 6 and fully leverage its features.
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 NSPersistentStoreDescriptionobject 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 NSPersistentStoreDescriptionyou 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
destroyPersistentStoremethod.
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 FileManagerto 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 replacePersistentStoremethod on thepersistentStoreCoordinatorto replace the current store with the one in the App Group folder.
- Call the destroyPersistentStoremethod on thepersistentStoreCoordinatorto 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 NSPersistentStoreDescriptionobject 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
.sqlitedatabase files in a tool like Core Data Lab by Betamagic.