GeneralAditya Uke

iOS App Groups: Share Data Between Your App and Extensions

How to set up iOS App Groups so a share extension, widget, and main app can share UserDefaults and files - group identifiers, entitlements, and the mistakes that break it.

A share extension receives the data. Your app is what saves it. On iOS those are two different sandboxes that cannot read each other - and an App Group is the sanctioned way to give them storage they can both reach.

Every target is its own sandbox

An iOS app runs in a container no other app can read. What surprises people is that this applies inside your own product too. A share extension, a widget, a notification service extension, and the app that ships them are separate processes with separate storage.

So the moment an extension needs to hand something to the app, there is nowhere to put it. The extension can write to disk all it likes; the app is looking at a different disk.

What an App Group is

An App Group is a container iOS makes visible to every target you assign to it. Join a target to the group and it gets:

  • a shared UserDefaults suite, for small values
  • a shared directory on disk, for files
  • and therefore a shared SQLite or Core Data store, if you need one

Two constraints are worth knowing up front. Every target in a group must belong to the same development team - you cannot share with another company’s app. And the container is local to the device: it is not iCloud, and nothing in it syncs anywhere.

The group identifier

Groups are registered in the Apple Developer portal under Identifiers → App Groups. The identifier must begin with group. - Apple enforces that prefix. The rest is yours, reverse-DNS by convention, and globally unique across every developer account.

group.ai.systenics.notes
└────┘ └───────────────┘
required  yours, must be globally unique

The useful convention is group. plus your app’s bundle identifier. Resist appending something like .sharedstorage - the string ends up in the entitlements of every target, in the portal, and at every call site, and a second group is rarely needed.

A group is not an App ID

These are three separate registrations in the portal, and the group is a different kind of identifier from the other two:

Portal section Identifier What it is
Identifiers → App IDs ai.systenics.notes the app
Identifiers → App IDs ai.systenics.notes.share the share extension
Identifiers → App Groups group.ai.systenics.notes the shared container

You need all three. Registering the group is only half of it - the App Groups capability then has to be enabled on both App IDs, with the group ticked in each.

How the data actually crosses

┌──────────────────┐   ✗ no direct access   ┌──────────────────┐
│ Share Extension  │ - - - - - - - - - - -> │     Your App     │
│   own sandbox    │                        │   own sandbox    │
└────────┬─────────┘                        └─────────▲────────┘
         │ writes                                     │ reads
         │        ┌──────────────────────────┐        │
         └───────>│   App Group container    │────────┘
                  │ group.ai.systenics.notes │
                  └──────────────────────────┘

Shared storage is not the only thing a group buys you. Apple documents that members of an app group can also communicate through IPC - Mach IPC, XPC, POSIX semaphores and shared memory, and UNIX domain sockets - with the group identifier forming the prefix of the service name. That matters for two processes running at the same time, which a share extension and a backgrounded app generally are not. For the share-then-save flow, the container is the crossing point, and any signal you send alongside it should carry a key at most - never the payload.

A worked example

A share extension takes a URL from the system share sheet and hands it to the app. Both targets declare the same group in their entitlements - this is what actually grants access, and it must match the portal registration exactly.

Notes.entitlements and NotesShare.entitlements

<key>com.apple.security.application-groups</key>
<array>
    <string>group.ai.systenics.notes</string>
</array>

Extension - write, then finish

// Runs in the extension's process.
let defaults = UserDefaults(suiteName: "group.ai.systenics.notes")
defaults?.set(sharedURL.absoluteString, forKey: "pendingURL")

// Hand control back. The URL stays in the container until the app reads it.
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)

App - read, then clear

// Runs in the app's process, on launch or resume.
let defaults = UserDefaults(suiteName: "group.ai.systenics.notes")

if let pending = defaults?.string(forKey: "pendingURL") {
    defaults?.removeObject(forKey: "pendingURL")  // consume once
    save(pending)
}

UserDefaults is right for a URL or a flag. For anything with weight - an image, a clipping, a database - write into the group’s directory instead and pass only the filename:

guard let container = FileManager.default.containerURL(
    forSecurityApplicationGroupIdentifier: "group.ai.systenics.notes"
) else { return }

let fileURL = container.appendingPathComponent("clipping.json")
try data.write(to: fileURL)

Both targets resolve that same directory, so the app opens the file the extension just wrote. This is also how a WidgetKit widget reads data it never fetched itself.

If the app and the extension might touch the same file at once - and they might, since an extension can run while the app is suspended in the background - coordinate the access with NSFileCoordinator rather than assuming your writes are serial.

A share extension cannot simply launch the app

It is tempting to finish the extension by opening a custom URL scheme so the app comes forward and reads the value immediately. Be careful here, because this is the least supported part of the whole flow:

  • UIApplication.shared is unavailable in an app extension, and reaching it through the Objective-C runtime is explicitly not supported by Apple.
  • NSExtensionContext.open(_:completionHandler:) exists, but support is per extension point, not universal. Apple’s own wording is that “each extension point determines whether to support this method,” and that “in iOS, the Today and iMessage app extension points support this method.” Share extensions are not on that list.
  • Apple’s Developer Technical Support has said on the forums that there is no supported API for a share extension to directly launch its containing app.

The dependable design is the one above: the extension writes to the container and finishes. The app picks the value up the next time it launches or returns to the foreground, which is exactly what the read above is placed to catch. Treat launching the app from a share extension as a nice-to-have you verify on your target iOS versions, not as the mechanism your feature depends on.

Setting one up

Order matters here - the entitlement is checked against the portal at signing time.

  1. Register the group under Identifiers → App Groups as group.your.bundle.id.
  2. Open each App ID, enable the App Groups capability, and tick the group. Do this for the app and every extension.
  3. In Xcode, add the App Groups capability to each target under Signing & Capabilities and select the group. With automatic signing, Xcode can perform steps 1 and 2 for you - but registering identifiers requires the Account Holder or Admin role, so a Developer-role account will have to ask someone to create the group first.
  4. Regenerate provisioning profiles so they carry the new capability.
  5. Build. A mismatch between the entitlement and the portal usually surfaces at signing or install time. The quieter failure is a group that is provisioned correctly but that a target never actually joined - that one compiles, installs, and simply reads nothing.

What usually goes wrong

  • Only one target joined the group. The extension writes, the app reads nothing. Both sides need the entitlement.
  • The identifier drifted. A typo between entitlement and portal is a signing error - “doesn’t support the … App Group” - not a runtime bug.
  • A missing group. prefix. Apple rejects the identifier outright.
  • Free provisioning. App Groups is not available through the basic Personal Team workflow. Expect to need an Apple Developer Program membership for real App Group work.
  • Expecting a sync. The container is device-local. Two devices with the same app share nothing.
  • Assuming the Keychain works the same way. It does not, though the two are related: a group you register on the Developer site also acts as a keychain access group. Credentials still go through the Keychain APIs and the keychain-access-groups entitlement, not the shared container.

Two footnotes

macOS has a second identifier format, but the guidance has flipped. You may still see TEAMID.groupname described as the Mac convention. Apple’s current recommendation is the opposite: use group. prefixed identifiers on macOS too. The team-ID form is still supported, and has the one convenience that you need not register it on the Developer site - but a group. identifier does have to appear in the provisioning profile.

Drop UserDefaults.synchronize(). Calling it after a write is a habit carried over from much older SDKs. It has been unnecessary for years, and plenty of sample code still does it.

A Quick Summary

Every target you ship is its own sandbox. A share extension, a widget, and the app that ships them are separate processes with separate storage, so an extension that receives data has nowhere to put it that the app can see. An App Group is the sanctioned way across: a container iOS makes visible to every target you assign to it, giving you a shared UserDefaults suite for small values and a shared directory for anything with weight.

Getting one working is mostly registration discipline. The group is a third identifier alongside your two App IDs, it must start with group., and the capability has to be enabled on every App ID and declared in every target’s entitlements. A provisioning mismatch will usually stop you at signing, which is the good outcome - the bad one is a group only half the targets joined, where everything builds and the app quietly reads nothing.

The design that holds up is the boring one. The extension writes into the container and finishes; the app reads and clears the value the next time it becomes active. Resist building on a share extension’s ability to launch its containing app - there is no supported API for it - and remember the container is device-local. It is not iCloud, and nothing in it syncs anywhere.


More on this topic:General