iOS Native Content Sharing Halp Plz

I can’t get the native content sharing to work for the life of me. I am saving a .wav file to the security group, then I hit the bit of code I essentially copied straight from the demo projects. I verify that the write is correct and the file exists in the sample writing dialogue (just by way of pointing out that I’m still trying to get this to work and haven’t put in any idiot proofing yet.)

File tmpWave = tmpFile.getChildFile("temp.wav");
processor.saveSampleFile(tmpWave);
Array<URL> urls;
urls.add ({ tmpWave.getFullPathName() });
ContentSharer::getInstance()->shareFiles (urls, [] (bool success, const String& error)
	{
		auto resultString = success ? String ("success") : ("failure\n (error: " + error + ")");
		AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, "Sharing Files 
            Result","Sharing files finished\nwith " + resultString);
	 });

So the .wav file is correctly created and exists. When I step through with the debugger, I can see that the array urls has one item, and that item is the full path name to the .wav file. But when it gets to line 54 in juce_ios_ContentSharer.cpp, isLocalFile() returns false, so it goes through that next series of creation steps in the else. As you can see, it appears to do this correctly:

Then it appears to hit line 75, but the nativeFilePath isn’t changed from nil, thus ensuring a skip of line 81, which we need for the Share dialogue to work. As a result, it throws an assertion that I’m trying to share nothing.

Firstly, I’m following the example directly, in which the URL array for the constructor of the Sharing dialog is populated with Files. Now, I know from experience that that sort of frippery will work on macOS, but can be semi-hazardous on iOS. But how would I get a true URL to that File I just created, and would it matter?

Secondly, wtf?

EDIT
In reading through the method for this, I think that we’re in another situation where, the way this is coded, it will work in standalone but not AUv3. Without a Files dialogue, an AUv3 can only write to the security group. The way the array is created in juce_ios_ContentSharer.cpp, it appears that it can only accept URLs from the main bundle or the file system, but not the security group. (pathForResource doesn’t recognize the path to the security group as valid, thus ensuring that the nativeFilePath remains nil.)

That being said, this will almost certainly assert later on (in the same manner than an AUv3 asserts if the third bool in a native saving dialog is false) because as it is written it isn’t owned by the Editor, but rather the view that contains it.

So, unless some intrepid soul wants to tackle this problem, I’ll just use a Files dialog, which I know works, albeit not as dope.

1 Like

Hi @crandall1, have you managed it to work? I’m struggling with sharing midi file from AUv3. The plugin is getting frozen if I’m trying to open Share dialog, as well as Files dialog (I use plain FileChooser). I feel like the problem is related to the modal state. It looks like UIViewController is created but doesn’t appear and the whole UI stuck at this point. Is it actually possible to save/share simple file from AUv3 with native iOS views?
Thank you

The final word, currently, is that the Sharing feature is totally unavailable to an AUv3.

You can open a Files instance, though. You just have to remember that you can’t go above the main UIView of the AUv3. The sandboxing prevents this. Any modal window, including Files, must be owned by the editor. In real JUCE terms, you do it like this:

exporter.reset(new FileChooser("Export to...", tmp, "", true, false,
			JUCEApplicationBase::isStandaloneApp() ? NULL : this));
			exporter->launchAsync (FileBrowserComponent::saveMode |
							       FileBrowserComponent::canSelectDirectories |
								   FileBrowserComponent::filenameBoxIsReadOnly,
								   [&tmp] (const FileChooser& chooser)
			{
				if (chooser.getResult().exists())
				{
					auto chosen = chooser.getResult();
					if (chosen.isDirectory())
					{
						bool result = tmp.copyFileTo(chosen);
						if (result && tmp.hasFileExtension("zip")) tmp.deleteFile();
					}
				}
			});

This triggers an std::unique_ptr<FileChooser> named exporter for saving asynchronously (it is very slow if you don’t do it async) that is owned by the window it is spawned in. You put your operations in the lambda.

AFAIK, this is the only way to safely move files in and out of the AUv3’s sandbox. The major downside is that, since the editor (or whatever component you triggered it from) owns the FIles instance, the Files doesn’t go fullscreen modal. It is only modal to your AUv3’s frame.

Note in particular that JUCEApplicationBase::isStandaloneApp(). If you’re doing only AUv3 and not a standalone, that’s where the trick is. NULL is “open a Files instance normally.” This will be fullscreen. “this” is the component where you’re triggering the Files instance from. In an AUv3 situation, it has to be this way. NULL will result in either a crash or nothing happening.

If you get any of the Audio Damage apps that have a Files dialogue (currently Enso, Quanta, and Continua) you can see how this works in real life. That bit of code above is taken directly from Continua’s “Export” function in the Presets menu.

2 Likes

Amazing! thank you for putting this all together. Sending the parent component to FileChooser constructor thing did the trick. I have a bit different implementation, as I’m exporting file, but not folder. This bunch of code runs only for AUv3, as for standalone I use ContentSharer:

fileChooser.reset (new FileChooser ("Export sequence as MIDI file..",
                                    tmpFile, String() , true, false, this));

fileChooser->launchAsync (FileBrowserComponent::saveMode |
                          FileBrowserComponent::canSelectDirectories |
                          FileBrowserComponent::filenameBoxIsReadOnly,
                          [this, tmpFile] (const FileChooser& chooser)
                          {
                              auto chosen = chooser.getResult();
                              if (chosen.exists())
                                  tmpFile.replaceFileIn (chosen);
                              
                              tmpFile.deleteFile();
                          });

The other thing that puzzled me for some time that temporary file should exist before running Files dialog. I tried to create midi file in lambda (to avoid saving it if user cancels export), but I got empty output files. Looks like iOS looks for already present file in the app group folder, creates zero size file in destination and then provides a “tunnel” to copy your temp file to selected folder.

4 Likes

Yeah, that puzzled me for a while as well. I tried to make the export file in the lambda too, and was all “wtf?” for a bit. So I just make it in the app security group, then move it. That’s why that deleteFile is in there. If the user exports a single preset, it just copies it. If they export a folder of presets, it zips it, then copies it, then deletes the zip. This actually fits better for our normal method, since we do desktop as well. In the actual code, that bit above is wrapped in #if JUCE_IOS.

I just realized that I need to delete the zip whether or not it is successful.

Thought I’d post a related topic - it may be the solution for getting ContentSharer to work in AUv3s among other things.

1 Like