Apple’s “app group” technology allows a collection of sandboxed macOS or iOS apps from the same development team to all communicate with each other, coordinate functionality, share resources, and/or minimize redundancies. Notice that I said that I can get sandboxed apps to communicate with each other. But isn’t sandboxing a security mechanism meant to keep apps isolated from each other? Yes and no. Apple realized at some point that it would allow apps developed by the same team, with the same Team ID — built by the same people and thus hopefully less risky — to intercommunicate. I’m glad they did as the usefulness of app groups outweighs the dangers.
As long as all developers involved in creating apps meant to be part of an app group can securely share a Team ID from an Apple Developer portal account, they can write apps that can transcend sandboxing. A company may carefully decide to share its Team ID with a trusted partner company, not just with different teams within its own organizational structure.
Member apps of an app group share access to a special group container, a shared folder structure, whose root folder has the same name as the app group ID. In the macOS sample app that accompanies this tutorial, several apps share a preference stored in a .plist
file that lives in the special group container. The preference is the background color to be used by app group apps’ UIView
instances. If one app sets (writes) this shared background color preference to, say, the color green, other app group members read this preference and can change their view layer backgroundColor
property to NSColor.green.cgColor
. By starting an app group, I’ve created the beginnings of a group of related apps sharing — centralizing — settings like view color scheme, even if all the apps are sandboxed.
You can already imagine the possibilities: shared preferences, shared licensing, shared caches, and/or shared databases. Apple says this capability “allows the apps within the group to share Mach and POSIX semaphores and to use certain other IPC [interprocess communication] mechanisms among the group’s members.” So, for example, you can have member apps performing thread-safe operations on shared resources.
Today, I’ll walk you through the configuration and encoding of a macOS app group whose members communicate through a shared instance of UserDefaults
, more commonly known as user preferences. One app allows me (and my users) to pick a view background color — like a theme color — and write it to my shared UserDefaults
. The other app can read that same shared preference and update its view’s background color to the currently saved value. This process is dynamic. As I change the theme color in one app, the other app can update its view’s background color immediately. We’ll build these apps and app group together in this tutorial. You can download the sample apps here. Here’s a video of my two sample apps coordinating together:

Probably one of the most intriguing aspects of app groups is that their commonly shared container, a mini folder and file system, transcends sandboxing. To highlight the flexibility of app groups on macOS, one of my sample apps is sandboxed while the other is not. On iOS, remember that all your apps are sandboxed.
App groups are a great feature, but unfortunately they’re not well-documented and therefore generally not well understood. For example, Apple has a section in it’s macOS developer documentation called “Adding an App to an App Group” which strongly implies that all you have to do is add an entitlement to your app’s project. In another developer document, Apple states “You control the groups that your app belongs to by manipulating its entitlements.” No mention is made of a very crucial step: registering your app group ID under your account on the Apple Developer portal.
For awhile, adding an app group identifier to an Xcode project’s entitlements was sufficient in macOS to create and join an app group. Only iOS required that developers have an account with Apple and that they register the app group ID on the Apple Developer portal. That requirement now applies to macOS app groups, too.
If you have a number of related apps, especially ones you sell as part of an App Store bundle, then app groups might be for you. Keep in mind that apps can be members of more than one app group, too.
Let’s walk though all the required steps to create two macOS apps that demonstrate app groups — and note that almost all the content herein applies to iOS app groups.
Registering your app group’s ID
The first thing to do when creating an app group is to register an app group ID. Have a copy of your Apple Developer account’s Team ID ready, like copied into your clipboard. In the screenshots shown below, you’ll substitute your own Team ID where I’ve redacted my own. Let’s get started:
1. Login to your Apple Developer account;
2. Select Certificates, IDs & Profiles from the left sidebar or the center of the screen;
3. You’ll land on the Certificates screen by default, but select Identifiers from the left sidebar;
4. Click the dropdown labelled App IDs, like so:
5. Select the App Groups option from the dropdown;
6. Now that you’re on the Identifiers for App Groups page, click the plus-sign-inside-the-circle button;
7. On the Register a New Identifier page that comes up, select the App Groups radio button, like this:
8. Click the big, blue Continue button on the upper right-hand side of the same page;
9. You’ll land on the Register an App Group page where you’ll get ready to fill in the Description and Identifier fields;
10. Type in a meaningful Description and type/paste your Team ID into the Identifier field;
11. Notice that “group.” is prepended to the Identifier, and that I added some meaningful text, following Apple’s suggestion, like this:
12. Click the big, blue Continue button;
13. You can review your new app group ID, are given the chance to click Back to make changes, but we’ll select the Register button; and,
14. Now back on the Identifiers for App Groups page, you’ll see that you’ve created a new app group.
The app group’s shared container
By definition, an app group is a collection of two or more apps. Whichever app that is a member of the app group, and runs before any of the other member apps run, creates what’s called the group’s “shared container.” It does this once unless someone deletes the container, in which case macOS recreates the container when it is next referenced by one of the group’s member apps. That container is a mini folder/file system like those that macOS provides for sandboxed apps. It’s a bit like the folder system that macOS builds for each user on a Mac or MacBook. Every shared container is stored in the macOS file system in a folder that has the same name as the app group ID, like group.TEAM_ID.com.domain.MyAppSuite
. That folder is always stored in the macOS file system in ~/Library/Group Containers/
. So the full container path is ~/Library/Group Containers/group.TEAM_ID.com.domain.MyAppSuite
. Here’s what the shared container looks like right after it’s created, including the preferences .plist
:
The app group has its own user preferences data store, accessible via a special instance of UserDefaults
. These preferences are stored in a .plist
file just like they are for all macOS apps. In this tutorial, those preferences are stored in the file at this path:
1 |
~/Library/Group Containers/group.YOUR_TEAM_ID.com.domain.MyAppSuite/Library/Preferences/group.YOUR_TEAM_ID.com.domain.MyAppSuite.plist |
All apps in the app group have read and write access to the shared container, including access to the shared preferences.
A word about my sample code
My code is semantically clear and well commented — but simple. You shouldn’t have any trouble reading it and understanding it. Remember that my code and this accompanying article are pedantic in nature. We’re talking about app groups, not about using MVC, robust error handling, testing, etc.
Let talk about the numbering of my comments, mainly the fact that I’ve shown them out of sequence, starting at “3a)” and not with “1)”. Let me explain: I’m introducing the code in the order I feel best for my tutorial’s flow, especially regarding what code I want to explain first, what to explain second, all the way to what to explain last.
So initially, please read the code, ignoring the comment numbers until you’ve read the whole article. Once you’ve got your Team ID, your app group ID, and you’ve got both apps configured with the app group ID, you can build and run them.
Then read the code with comments in numeric sequence starting with “1)” all the way through “4g)”. You’ll see that the sequencing of the comment numbers makes sense in terms of the overall flow of the way my two apps work together.
“FirstApp” – Creating an app that is part of an app group
Let’s walk through building my app group’s first app, ingeniously named “FirstApp.” It reads from and writes to the shared container. My FirstApp writes a color to the shared container’s preferences so that my “SecondApp” can read that preference, and thus we have interprocess communication.
Open up Xcode (I’m using 13.2.1) and create a new application based on the macOS App
template.
Follow these steps in TARGETS -> Signing & Capabilities:
1. Tick the Automatically manage signing checkbox and then set the Team and Bundle Identifier appropriately;
2. Make sure to that you have the App Sandbox and Hardened Runtime capabilities;
3. Click the + button next to Capability and add the App Groups capability by dragging it in from the Library, like this:
4. Add your app group ID to the App Groups slot. Where it says Add containers here, press the + button and add your app group ID:
Remember that your app group ID should look like this:
1 |
group.YOUR_TEAM_ID.com.domain.MyAppSuite |
Your app group ID is actually an array element in the .entitlements
file.
Note that I specifically sandboxed this app to show you that an app group app can read the shared container, which is outside of this app’s sandbox.
Everything you need to see is in the ViewController.swift
file. Let’s briefly walk through the most important code in FirstApp — a few selected snippets.
Let’s examine how I write user-selected color values from my app’s NSComboBox
to the app group’s shared preferences. The interprocess communication comes into play when my SecondApp, described below, reads that color preference. Notice that I’m getting a special type of reference to UserDefaults
by calling its init(suiteName:)
initializer, where the full definition is init?(suiteName suitename: String?)
, and note the optional return value. I pass my app group ID to the argument labelled suiteName
. I access the user preferences associated with my app group by initializing UserDefaults
specific to my app group’s ID, stored in the shared container. Then I can set a key/value specific to that app group:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
... /** 3a) Write user preference to shared container. */ func setPreferenceValue(_ value: Any?, forKey key: String, in appGroup: String) { // 3b) If we can access preferences to our app group... if let groupUserDefaults = UserDefaults(suiteName: appGroup) { // 3c) Write the value for the given key // to our shared container. groupUserDefaults.set(value, forKey: key) print("Wrote to shared user defaults.") } } // end func setPreferenceValue ... |
If you’re going to do something with your app group like store common files or maintain a shared cache, you’ll need to be able to get a valid macOS file system path to read from, and possibly write to, files/folders in the shared container. To get a URL to the root folder of your shared container, you use the containerURL(forSecurityApplicationGroupIdentifier:)
instance method of FileManager
, passing it your app group ID. Notice that instead of testing to see if a file exists first before reading from it, I proactively read from it using NSData
and then check for a return value:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
... /** 4a) If you're going to manage and access common resources or assets in the shared container, remember that "It’s far better to attempt an operation (such as loading a file or creating a directory), check for errors, and handle those errors gracefully than it is to try to figure out ahead of time whether the operation will succeed." - returns: True if plist exists; false if it doesn't exist */ func sharedPreferencesPlistExists() -> Bool { var containerExists = false let sharedFileManager = FileManager.default /* 4b) "In macOS, a URL of the expected form is always returned, even if the app group is invalid, so be sure to test that you can access the underlying directory before attempting to use it." */ let sharedContainerFolderURL = sharedFileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupID) // 4c) Now we build a standard path ("Library/Preferences/") // to the preferences data store file (plist). Note // the format of the plist's name. let sharedContainerPrefsPlistURL = (sharedContainerFolderURL?.appendingPathComponent("Library/Preferences/group.YOUR_TEAM_ID.com.domain.MyAppSuite.plist"))! // 4d) Try to read data from the preferences // plist file. let sharedContainerPrefsPlistData = NSData(contentsOf: sharedContainerPrefsPlistURL) // 4e) If the file exists... if let fileData = sharedContainerPrefsPlistData { // 4f) if the plist file has contents (bytes)... if fileData.length > 0 { // 4g) We know that the plist is valid. print(".plist file size: \(fileData.length)") containerExists = true } } return containerExists } // func sharedContainerExists() ... |
Upon clicking the “Check for .plist” button in FirstApp, the console shows:
1 2 |
.plist file size: 90 Shared plist created. |
“SecondApp” – Creating another app so we have an app group
To restate what I would hope is now the obvious, “an app group is a collection of two or more apps.” SecondApp is, well, the other app that makes up my little app group. SecondApp reads a preference from the shared container. That preference for color is written by FirstApp. SecondApp sets its view controller’s NSView
background color to the shared container’s preference and thus we have interprocess communication.
Open up Xcode (I’m using 13.2.1) and create a new application based on the macOS App
template.
Follow these steps in TARGETS -> Signing & Capabilities:
1. Tick the Automatically manage signing checkbox and then set the Team and Bundle Identifier appropriately;
2. In this case, make sure to remove the App Sandbox capability;
3. Make sure the app has the Hardened Runtime capability;
4. Click the + button next to Capability and add the App Groups capability by dragging it in from the Library, like this:
5. Add your app group ID to the App Groups slot. Where it says Add containers here, press the + button and add your app group ID:
Remember that your app group ID should look like this:
1 |
group.YOUR_TEAM_ID.com.domain.MyAppSuite |
The app group ID is actually an array element in the .entitlements
file.
Everything you need to see is in the ViewController.swift
file. Let’s briefly walk through the most important code in SecondApp — just one selected snippet. Again, I’m getting a special type of reference to UserDefaults
by calling its init(suiteName:)
initializer with my app group’s ID as an argument. This is how I read the user preference for the color that was set in FirstApp. I then change my main window’s view background color to the preferred color. Here’s the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
... func setViewBackgroundColor() { // 1) Get a reference to the shared user defaults for the // app group we created. if let groupUserDefaults = UserDefaults(suiteName: appGroupID) { // 2) Read the value for the "BackgroundColor" key stored // in our app group shared container's preferences // (user defaults). if let backgroundColor = (groupUserDefaults.object(forKey: "BackgroundColor")) as? String { // 3) Set the background color of our NSView to // the value we read from the our shared container's // preferences. self.view.wantsLayer = true self.view.needsDisplay = true if backgroundColor == "Red" { self.view.layer?.backgroundColor = NSColor.red.cgColor } else if backgroundColor == "Green" { self.view.layer?.backgroundColor = NSColor.green.cgColor } else if backgroundColor == "Blue" { self.view.layer?.backgroundColor = NSColor.blue.cgColor } else { self.view.layer?.backgroundColor = NSColor.gray.cgColor } print("Read background color, \(backgroundColor), from shared user defaults.") } // end if let backgroundColor =... } // end if let groupUserDefaults =... } // end func setViewBackgroundColor() ... |
Note that I specifically didn’t sandbox this app to show you that macOS apps don’t necessarily need to be sandboxed (especially those distributed via 3rd party signing) and that non-sandboxed apps can access their app group’s shared container.
.plist
will be there from the start. Since I wanted to keep this project simple, I left that step out, and I also wanted to show you how an app group can go wrong if that initial .plist
file is not there from the beginning.Installing and distributing your macOS apps
Remember that if you want to install and/or distribute macOS apps like the ones I discussed here, you have two choices. You can go the 3rd party route and sign and notarize your apps outside of the Mac App Store or you can submit your apps for review by staying within the Mac App Store. macOS apps are more flexible in terms of distribution than iOS apps. I’ll be publishing a book on Amazon Kindle on how to distribute macOS apps outside of the Mac App Store. I’ll also be offering iOS courses on Udemy. Come back to look for it soon!
Conclusion
I hope you see the benefits that can be gained by use of app groups, whether it be in iOS or macOS. You can control and update your apps more easily by centralizing shared resources. You can eliminate redundancies. And you can get really advanced by using technologies like shared memory (as long as you remember to keep things thread safe). I could go on and on about the benefits of app groups, but I hope you’ll see that, for a relatively small price, you can get a high return on investment.