How do app groups, shared containers and keychain access groups let an app and its extensions share data, and what goes wrong?
Predict first
A process asks for an app-group container without the capability. What does it get?
import Foundation
print(FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.example") == nil)
- true
- false
Mental model. An app group is a locker in the lobby that every shop in the building has a key to. Without the key (the entitlement) the locker does not exist for you; with it, anyone can open it at any time, so leave notes in a labelled folder and never leave valuables.
Because an extension is a separate process with its own sandbox, you share through capabilities that create common storage. An App Group (a capability with an identifier such as group.com.example.app, enabled on every target that needs it, with the identifier also registered in your developer account) provides a shared container directory (FileManager.default.containerURL(forSecurityApplicationGroupIdentifier:)) and a shared preferences domain (UserDefaults(suiteName: "group.com.example.app")). Without the entitlement, containerURL(forSecurityApplicationGroupIdentifier:) simply returns nil: a very common first bug, caused by forgetting to add the group to the extension target or having mismatched identifiers, and a nil you then force-unwrap in the extension. What to put there: small preferences and flags (UserDefaults), files and caches (images, JSON the widget renders), and databases (SQLite, Core Data and SwiftData can point their store at the shared container, with the WAL-mode and locking concerns from the persistence topic). The Keychain is shared by an access group (the Keychain Sharing capability; items stored with the same kSecAttrAccessGroup are visible to every target that lists it), which is where tokens and credentials belong, not in the shared defaults file.
The pitfalls. Concurrency across processes: the app and an extension can read and write at the same time, so use atomic file writes, SQLite transactions and WAL, NSFileCoordinator where a file is truly contended, and never assume a read sees the latest write immediately; UserDefaults across processes is eventually consistent (call synchronize() is mostly historical, observe changes with KVO or Darwin notifications). Cross-process signalling: use Darwin notifications (CFNotificationCenterGetDarwinNotifyCenter) or WidgetCenter.shared.reloadAllTimelines() to tell the other side something changed, since there is no shared memory. The 0xdead10cc crash: if an app or extension is suspended while holding a file lock or an open SQLite transaction in the shared container, the system terminates it (see the platform topic), so finish writes and release locks before backgrounding. Data contracts: the app and extension may be different versions around an update, so version the shared schema, decode defensively and ignore unknown keys, and never share raw model types that change per release. Privacy and security: the container is readable by every target in the group, so do not put secrets in it, and minimise personal data that a widget on the Lock Screen might reveal. Size: shared data that a memory-limited extension must load should be small and pre-rendered (a thumbnail, not the original), prepared by the app ahead of time.
Without the app-group entitlement the system does not provide a container, so the call returns nil; the capability must be added to every target that needs it.
Common traps
- Enabling the app group on the app but not on the extension target. The container is
nilin the extension; add it to every target with the same identifier. - Keeping tokens in the shared
UserDefaultsfile. Use the Keychain with an access group.
They might follow up with
- How does an extension tell the app something changed? Darwin notifications, or a reload request such as
WidgetCenter; there is no shared memory. - What goes wrong with a shared SQLite database? Concurrent writers and locks held across suspension; use transactions and WAL and finish writes before backgrounding.
Related questions
- 19.1.1 What is an app extension, how is it launched, and what limits do extensions run under?
- 19.4.1 How does a WidgetKit widget work: providers, timeline entries, reload policies, families and the refresh…
- 6.8.2 How do keychain access groups differ from app groups?
← What is an app extension, how is it launched, and what limits do…