Using Core Data in iOS app extensions with a shared App Group store

This article focuses on one narrow setup problem: a regular iOS app can usually reach Core Data through its AppDelegate, but an App Extension needs the persistent store moved into a shared App Group container so the main app and extension targets can open the same database.

Examples of iOS app extensions such as iMessage, Today, and Siri extensions

This article is useful because it solves one very specific gap between normal app code and extension code.

In a regular iOS target, Core Data is often treated as an app-owned singleton that lives behind the AppDelegate or a persistence controller. That works as long as only the main app target needs the store. The moment an iMessage extension, Today extension, Siri extension, or another app extension needs the same data, the default file location stops being enough.

The article does not try to redesign Core Data architecture from scratch. It takes a smaller path: move the SQLite store into an App Group container, then create a custom persistent container class that points Core Data to that shared directory.

Scope This is a storage-location setup guide. It is about making one Core Data store reachable from the main app and its extensions, not about model migration strategy or full multi-process concurrency design.

The article starts from the normal app-only pattern: grab the persistent container from the app delegate and use its viewContext.

The baseline code is simple and familiar:

let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext

The point of the article is not that this code is wrong. It is that it assumes the database lives where the main app expects it to live. If an extension target also needs to open the store, you first need a file location that both the app and the extensions are entitled to read.

Turn on App Group for the main app and every extension target, then assign the same group identifier everywhere.

The first project-level step stays in Xcode's Signing & Capabilities area. Add the App Group capability to the main app and to each extension target that should see the same database. Then choose the same group identifier for all of them.

Adding the App Group capability in the Signing and Capabilities tab
Add the App Group capability from the target capabilities screen.
The same App Group identifier assigned across the app and extension targets
Use the same group identifier for the app and every extension that should share the store.

Create a custom persistent container that overrides the default directory and points the SQLite file into the shared group container.

The central trick in the article is the custom container subclass. Instead of letting Core Data use its usual target-specific default location, override defaultDirectoryURL() and build the path from the App Group container URL plus the SQLite filename.

The article also calls out one practical detail: look up the .xcdatamodel name first. If your model is named Sakura.xcdatamodel, the Core Data stack name should be Sakura.

import Foundation
import CoreData

class NSCustomPersistentContainer: NSPersistentCloudKitContainer {
    override open class func defaultDirectoryURL() -> URL {
        var storeURL = FileManager.default.containerURL(
            forSecurityApplicationGroupIdentifier: "group.com.[App Group Name]"
        )
        storeURL = storeURL?.appendingPathComponent("Sakura.sqlite")
        return storeURL!
    }
}
Why this matters The move is not cosmetic. It relocates the persistent store to a sandbox location that the app and the extensions are both entitled to access.

Once the container class points to the shared directory, the rest of the Core Data access pattern looks familiar again.

The access code in this article is intentionally close to the regular app-only version. The difference is that the persistent container instance now uses the custom subclass, so loading the store resolves into the shared App Group path instead of the target's normal default directory.

lazy var persistentContainer: NSPersistentContainer = {
    let container = NSCustomPersistentContainer(name: "Sakura")

    container.loadPersistentStores { storeDescription, error in
        if let error = error as NSError? {
            print(error.localizedDescription)
        }
    }

    return container
}()

let context = persistentContainer.viewContext

That is the main value of the technique: after the storage location is corrected, the rest of the app and extension code can continue to interact with a standard Core Data context instead of learning a completely different persistence API.

The article is explicit about timing: make this storage-layout decision before shipping, not as a casual update to an app that already has user data.

This article contains two warnings that are worth preserving. First, it says to make this change during the first development stage of an unpublished app instead of casually introducing it later to an already-shipped product. Second, if user data already exists, take a separate backup before making important storage changes.

That caution is the right closing note. Moving the persistent store path is powerful, but it is also a real storage migration decision. The setup itself is small; the consequences of getting it wrong are not.