App Group Folder Access

I’m about to spend the next several days beating my head against the wall as I roll in my own App Group functionality for our iOS AUv3 releases; I just wanted to see if there’s any movement on the Roli front in this regard, because I would be sad to do it then find it in the next JUCE update. And I don’t want to be sad. I want to be glad.

In related news, has anyone else done this and wants to share a bit of code? I am wading in to waters that I’m not familiar with here, and don’t want to really reinvent the wheel.

Further to that, the NSURL is returning nil. As far as I can tell (and I have quite a bit of experience in this) all my provisioning and signing is correct, the group is in the Developers portal as an acceptable group and shows in the list, and everything is correctly entitled.

NSFileManager* fm = [NSFileManager defaultManager];
NSURL *containerURL = [fm containerURLForSecurityApplicationGroupIdentifier:@"group.com.audiodamage.Eos2"];
File startDir([containerURL.absoluteString UTF8String]);

Any thoughts on what I’m doing wrong here?

In NOISE we don’t use the NSFileManager at all to locate shared resources. We simply use JUCE’s File::getSpecialLocation (File::invokedExecutableFile) which will return the path of the AUv3 if you’re code is executing inside the AUv3. You can then, for example, get the Resources directory of the parent app bundle. Something like this (not tested):

File getSharedResourceFolder()
{
    File bundle = File::getSpecialLocation (File::invokedExecutableFile).getParentDirectory();

    // macOS uses Contents/MacOS structrue, iOS bundle structure is flat
   #if JUCE_MAC
    bundle = bundle.getParentDirectory().getParentDirectory();
   #endif

    // appex is in a PlugIns folder inside the parent bundle
    if (SystemStats::isRunningInAppExtensionSandbox())
        bundle = bundle.getParentDirectory().getParentDirectory();

   #if JUCE_MAC
    bundle = bundle.getChildFile ("Resources");
   #endif

    return bundle;
}

If both the AUv3 and the parent app are in the same app group then both will be able to access this resource folder. Also note, that currently, there is no way in the Projucer to add your app/AUv3 to an app group. You need to do this manually in Xcode every time you re-save your .jucer file:

I think it also requires you to register the app group with Apple online somehow - but Xcode should complain when you try to tick the app group box and you haven’t set this up online correctly.

Okay, that returns the same path in both the appex and the app, so I think it’s working. It works in the simulator, but I can’t read/write on the device, so that makes me think my issue is entitlements. As usual. sigh

Thanks, Fabian!

Just an update for other people that may get led astray: the code that Fabian supplies above will not work in an iOS standalone/AUv3 group. After much experimentation, here is the correct method for obtaining a path to an app group on iOS.

  1. Create an App Group and provision it in the Capabilities tab of your targets. Ensure that both the App and the App Extension have the same App Group identifier. In theory, Xcode should do the magic words in your provisioning portal automatically.

  2. Change whatever your file manager file is from .cpp to .mm so you can use Obj-C++.

  3. Add these includes:

“Foundation/NSFileManager.h”
“Foundation/NSString.h”

The following code gets you a path that both the sandboxed AUv3 and the main application can write to.

NSFileManager* fm = [NSFileManager defaultManager];
NSURL *containerURL = [fm containerURLForSecurityApplicationGroupIdentifier:@"group.com.yourcompany.yourAppID"];
String tmp = ([containerURL.relativeString UTF8String]);
File startDir(tmp.substring(6));

Note the group identifier should be whatever you made in the Capabilities tab. The code Fabian supplied works fine on OS X and in the Simulator for iOS, but it does not work on a provisioned iOS app on a device.

4 Likes

Thanks for putting this online. This sheds some light on how to use AppGroups to share data between AUv3 and standalone. However how do you put data into the AppGroup container apart from creating files on runtime using the startDir directory? I am trying to avoid a duplicated resources folder as I am storing samples there, but I can’t find any resources on how to populate the AppGroup container.

This puzzled me as well. I’m just using App Groups for presets currently, so the factory presets (which appear to the user to be in with the user presets in the App Group folder) are just stored in the binary blob and read from there.

As far as I can see, there are two ways to accomplish it, though: store the files in the blob and write them to the folder on first start, or download the files from a server on first start of the standalone. The former method would, of course, effectively double your drive footprint. If we were on a Mac, I would just put them in the /Resources folder of the bundle, then move them on first start, but we’re not on a Mac.

Anyone have any thoughts on a sexy way to do this?

What about deleting the resources from the appex target, and move everything to the shard app group container the first time the standalone is executed?

I think this would work, however it might require some changes to the Projucer (unless you want to redo this every time you resave the project):

  • allow the Resources folder to be copied only to the standalone location (maybe a drop down below the Resources folder field).
  • Add AppGroup ID property (maybe below the Development Team ID field)

Also a File method like File::getSharedResourceFolder() would be great with something like the code snippet from crandall, which could use a preprocessor definition for the App Group ID defined by the Projucer (just like Project_Version_String etc).

I think this is crucial for any kind of sample based instrument that uses the AUv3 extension feature. I am starting to think that Apple does this on purpose to force users to buy a bigger device :slight_smile:

Yeah, I think you’re on to the solution. All of us that build iOS AUv3 are sad that ProJucer doesn’t have App Groups. The number of times I’ve made a tiny change and submitted to the App Store and forgot to re-activate the App Groups is… well… I’d be embarrassed to say the number out loud, but it’s large. Once I even pushed an update without it. That is, of course, my fault, I’ll readily admit.

But yeah, you got the method, I think.

I’ve hacked around in the Projucer to enable these things. It almost works (I have to tick the AppGroup box in Xcode), but it definitely brings down the things to do). Here is a diff - unfortunately, I am still stuck on JUCE 4.3.1 so I can’t do proper pull requests):

AddAppGroupSupport.txt (7.7 KB)

(renamed to .txt because the forum doesn’t allow .diff files to be uploaded).

  • creates the entitlements file and the entry in the xcode project when an app group ID is supplied.
  • removes the resources from the app extension target (if enabled)
  • creates a preprocessor macro called APP_GROUP_IDENTIFIER which can be used to get the shared resource folder.

The nightmare continues, you can’t delete files from the Resources folder because they are code-signed which renders all efforts I made entirely useless.

I simply can’t believe there is no solution for this.

I don’t quite understand the fuss here. ROLI has an app with AUv3 in alpha and we use the code posted above to access our shared resources. The only thing that we need to do manually is click on the app group checkbox in xcode everytime we re-introjuce the project (we are currently working on a fix so that manually clicking this checkbox won’t be required anymore).

What exactly does not work with the code I posted?

1 Like

OK. Ed just pushed the Projucer changes to develop. 0e6699c is the relevant commit.

1 Like

Amazing, thanks. This is indeed a simple solution :slight_smile:

I was just getting intimitated by the apple docs which state:

Even though an app extension bundle is nested within its containing app’s bundle, the running app extension and containing app have no direct access to each other’s containers.

(taken from here)

And calling getParentComponent().getParentComponent() somehow qualifies as direct access :slight_smile: so I don’t know whether the docs are just plain wrong or if this is undefined behaviour which might be changed in the future.

However I just checked, the solution does indeed work for me (and also on a real device not just the Device Simulator like crandall observed).

And thanks for merging the changes.

Insert huge facedesk here.

I went back to the test app I’d made to work this out, and checked again, and it indeed works in the simulator and in OSX but not on a device. So I went through my implementation line by line. I discovered that instead of “groups.com.audiodamage.APPNAME,” I had named the app group “groups.audiodamage.com.APPNAME” which, not being a reverse domain, failed on the device.

I am an idiot. Disregard anything further I post.

3 Likes

Bumping this because I still can’t get it to work. As above, Fabian’s code works on the simulator but not on a device. Here’s where I create my directory:

#if JUCE_IPHONE
	File bundle = File::getSpecialLocation (File::invokedExecutableFile).getParentDirectory();
	if (SystemStats::isRunningInAppExtensionSandbox())
		bundle = bundle.getParentDirectory().getParentDirectory();
	userPresetFolder = bundle.getChildFile("Resources/Presets");
    if(!userPresetFolder.isDirectory())
        userPresetFolder.createDirectory();
#elif JUCE_WINDOWS

And here is my App Groups provisioning in Xcode…

And here is the path returned in the AUv3…

(The above is identical in the appex and app, but note that isDirectory() in the code returns false and the createDirectory() line is never hit on the device.)

This works fine in the simulator, but doesn’t work on the device at all. Anyone have even a faint idea of where I’m going wrong here. I have the App Group all created and linked in the dev portal, and everything seems fine. I’d really like to not have to spin off a separate .mm just for the iOS build. I’ve deployed 9 apps now with my code above, so I can always fall back to that, but I’d rather use the bespoke JUCE method, for obvious reasons.

did you solve this?

Nope. As far as I can tell, the method that Fabian supplies devolves to the correct folders (his code results in the root being the bundle, in both cases) but it clearly says you’re not allowed to write to a signed bundle in the Apple docs; the correct folder is the bundle’s container, which should have a folder called “Documents” in it for exactly this case. But the .appex can’t see this. It can in the simulator, but not on the device.

So, basically, despite what Fabian says above, what I’m seeing from the appex in a signed bundle is exactly what Apple says I should see, which is nothing, and I have exactly the permissions Apple says I should have, which is none. I’ll believe I can get to it from the JUCE filesystem when I see working, compiling sample code to that effect running on a device. :slight_smile:

In any event, I just used a somewhat more refined version of what I posted above. This:

#if JUCE_IPHONE
#include "Foundation/NSFileManager.h"
#include "Foundation/NSString.h"
#endif

And this in the constructor:

#if JUCE_IPHONE
	File result = File::nonexistent;
	NSFileManager* fm = [NSFileManager defaultManager];
	NSURL *containerURL = [fm containerURLForSecurityApplicationGroupIdentifier:@"group.com.audiodamage.Filterstation2Presets"];
	String tmp = ([containerURL.relativeString UTF8String]);
	File startDir(tmp.substring(6));
	userPresetFolder = startDir;
#elif JUCE_WINDOWS

And setting the .cpp to “compile as Obj-C++” in the folder settings.

image

Unpleasant, and I have to set that every time I touch ProJucer, of course, but it works.

3 Likes

thanks chris. Can i ask how you get your presets in the bundle/group folder? post build script?