Drag and Drop files to Juce component (iOS, AUv3)

Hi there. I’m looking for a way to implement native iOS file picker that allows drag and drop and a way to make a target out of Juce component. Take a look at this video please to get the idea: youtube link

I know I should deal with Obj-C native classes (which is going to be hard as I haven’t touched it for few years) and I’m wondering has this question been explored by anyone. Any advices on where to look in UI classes (both iOS and Juce) or general suggestions are welcome.

bump.

Dragging files to AUv3 is widely supported by many plugins on the market now. Is it really impossible to make FileDragAndDropTarget work on iOS?
Anyone had some experience with dragging files on iOS?

bump. Are we the only company getting tones of requests from users asking for native way of dragging samples onto iOS plugin?

I agree that out-of-the-box support in JUCE would be a good thing. It’s not actually that hard to implement yourself. I came up with a solution that doesn’t even require modifications to JUCE code. Basically you attach a UIDropInteractionDelegate to the editor’s peer and use FileDragAndDropTarget’s methods to interact with JUCE on a Component level.
The only tricky part is that drags sometimes are promises with contents created dynamically so you need a way to store samples that came that way. This was in the end a reason why i didn’t share the code on the forum as it quite specific to our application and it shares code with copy and paste support.

1 Like

Hi @luzifer, thanks a lot for your input! It gives me a new hope to implement this feature finally. May I ask you for a bit more detailed explanation on how do you attach UIDropInteractionDelegate to Juce code? I don’t ask for specific details, but if you can share some general code example that will be really helpful

I assume the object is composed into the editor class and watch for componentParentHierarchyChanged to attach to the peer. Something along the lines of:

void IOSFileDropInteraction::componentParentHierarchyChanged(Component &)
{
    auto peer = m_toplevelComponent->getPeer();

    removeInteraction();

    if(peer)
    {
        UIView *view = (UIView *)peer->getNativeHandle();

        auto dir = m_itemProviderTemp->getDirectory();

        auto delegate = [[DropDelegate alloc]
                         initWithComponent:m_toplevelComponent];

        auto interaction = [[UIDropInteraction alloc] initWithDelegate:delegate];

        [view addInteraction:interaction];
        m_interactionObject = interaction;
        m_peer = peer;
    }
}
1 Like

Thanks @luzifer. I’ve managed to implement it and get native callbacks.However when I’m trying to get current drag coordinates - they are always zero. I’m using locationInView method:

static UIDropProposal* sessionDidUpdate (id self, SEL, UIDropInteraction*, id<UIDropSession> session)
{
  auto view = (UIView *)peer->getNativeHandle();
  auto location = [session locationInView:view];
  NSLog(@"x:%d - y:%d", location.x, location.y);
}

I’ve tried also view.superview and [view.subviews lastObject]. Any ideas?
Does UIView object constructed by iOS ComponentPeer has correct screen bounds?

I’ve also tried Desktop::getInstance().getMouseSource(0) but seems like we can’t track finger coordinates if gesture starts in other process

No idea … i’m doing the same and get useful coordinates

Here is my working solution for Drag & Drop files for iOS. This is pretty much first Juce+Obj-C wrapper I made from scratch, so I would be happy to hear any comments if its ok in terms of ARC and thread safety.

Few details that might be useful:

  • The integration is pretty straightforward. Just attach the singleton to the peer using FileDropContainer::getInstance()->setParentComponent(getTopLevelComponent()); in your editor ctor and that’s all: regular Juce::FileDragAndDropTarget methods should be called from now on.
  • Until the object is dropped we don’t have access to the full file name, thus we can’t read extension, so the decision about accepting particular file type is made on the wrapper level via native method canHandleSession. All methods of Juce::FileDragAndDropTarget except filesDropped will get just file names without extensions.
  • StringArray passed to filesDropped method contains native iOS bookmarks converted to juce::String (with special prefix). Bookmarks recieved can be converted to juce::URL in order to access file and store the bookmark for later use. I have a separate juce::URL wrapper to handle bookmarks. More info on bookmarks can be found here and here. I can share my solution as well
  • This implementation accepts audio files. However, I couldn’t make it accept objects from Voice Memos yet. Perhaps, this is what @luzifer mentioned about accepting dynamically created objects, but for now no idea what native iOS class with audio data is passed.
  • In order to include MP3, FLAC and OGG to kUTTypeAudio (public.audio) I had to register them in PList:
<plist>
  <dict>
    <key>UTImportedTypeDeclarations</key>
    	<array>
      		 <dict>
			          <key>UTTypeIdentifier</key>
          			<string>org.xiph.flac</string>
			          <key>UTTypeDescription</key>
			          <string>FLAC Audio</string>
			          <key>UTTypeConformsTo</key>
			          <array>
				            <string>public.audio</string>
			          </array>
			          <key>UTTypeTagSpecification</key>
			          <dict>
				            <key>public.filename-extension</key>
				            <array>
					              <string>flac</string>
            				</array>
         			</dict>
		       </dict>
      		 <dict>
			          <key>UTTypeIdentifier</key>
          			<string>public.mp3</string>
			          <key>UTTypeDescription</key>
			          <string>MP3 Audio</string>
			          <key>UTTypeConformsTo</key>
			          <array>
				            <string>public.audio</string>
			          </array>
			          <key>UTTypeTagSpecification</key>
			          <dict>
				            <key>public.filename-extension</key>
				            <array>
					              <string>mp3</string>
            				</array>
         			</dict>
		       </dict>
       <dict>
			          <key>UTTypeIdentifier</key>
          			<string>org.xiph.ogg</string>
			          <key>UTTypeDescription</key>
			          <string>OGG Vorbis Audio</string>
			          <key>UTTypeConformsTo</key>
			          <array>
				            <string>public.audio</string>
			          </array>
			          <key>UTTypeTagSpecification</key>
			          <dict>
				            <key>public.filename-extension</key>
				            <array>
					              <string>ogg</string>
            				</array>
         			</dict>
		       </dict>
	    </array>
  </dict>
</plist>
3 Likes

good job! dragging from voice memos to sitala seems to work. i don’t think i had to do anything particular to do that though. for UTIs we just accept the root public.audio type.

Hi, thanks for sharing these, I tried the functions but got these 3 errors while compiling, any suggestions?

Undefined symbol: FileDropContainer::singletonHolder
Undefined symbol: FileDropContainer::setParentComponent(juce::Component*)
Undefined symbol: FileDropContainer::FileDropContainer()

BTW I’m using the entire editor to be the drop target.

@playpm My only guess would be to define IOS. As you can see I’m using #if IOS directive instead JUCE_IOS. This way I can add condition before including Juce headers and avoid ambiguity problems. It’s easy to define it via Projucer

Thanks, glad to learn a new thing, it worked!

However I got another issue, I found the filesDropped function will transform whatever is dropped to the bookmark in string format, is there anyway to let it remain an array of original file URLs in string format? Or is there anyway to transfer bookmark string into an URL?

I already have the functions for extracting the bookmark from an URL on iOS and also restore it into an URL, thought it would be a cleaner implementation.

(Sorry if this is a dumb question, I’m still new to C++ and not that familiar with Obj-C :joy:)

You can try to simplify [fileCoordinator coordinateReadingItemAtURL:] access callback and use something like container->draggedItems.set ((int)index, nsStringToJuce([newURL absoluteString]) to fill result array with just URLs.
I’m using bookmarks as I have my entire file management for iOS build on bookmarks and I’ve learned that it’s the only way to get access to the files and restore it later - just having path is not enough. At least seems like we need to generate bookmark and retain it once before working with URL path. I’m using the following method to transfer bookmark string to URL:

URL createFromBookmark (const String& bookmarkString)
{
    jassert (bookmarkString.startsWith(bookmarkStringPrefix));
    
    MemoryOutputStream stream { 2048 };
    Base64::convertFromBase64(stream, bookmarkString.substring(5));
    
    if (stream.getDataSize() == 0) return {};
    
    NSData* const nsBookmark = [NSData dataWithBytes:stream.getData()
                                              length:stream.getDataSize()];
    
    BOOL isBookmarkStale = false;
    NSError* error = nil;

    NSURL* const nsURL = [NSURL URLByResolvingBookmarkData:nsBookmark
                                                   options:0
                                             relativeToURL:nil
                                       bookmarkDataIsStale:&isBookmarkStale
                                                     error:&error];
    
    if (error == nil)
    {
        auto url = URL(nsStringToJuce([nsURL absoluteString]));
        if (isBookmarkStale)
           updateStaleBookmark ((void *)nsURL);
        else
        {
            [nsBookmark retain];
            setURLBookmark (url, (void *)nsBookmark);
        }
        
        return url;
    }
    else
    {
        [[maybe_unused]] auto desc = [error localizedDescription];
        jassertfalse;
    }
    
    return URL(lastKnownPathFromBookmark(nsBookmark));
}
String lastKnownPathFromBookmark (void* bookmarkData)
{
    NSDictionary *values = [NSURL resourceValuesForKeys:@[NSURLPathKey]
                                       fromBookmarkData:static_cast<NSData*> (bookmarkData)];
    
    return nsStringToJuce ([values objectForKey:NSURLPathKey]);
}

You may find similar methods in @adamwilson implementation for bookmarks in the topics mentioned above

3 Likes

Can’t thank you more for all of these!

I managed to pass original URLs through filesDropped() function. Then I found the URLs from iOS dropping seems not readable.

The dropped url looks like this in string:

file:///private/var/mobile/Containers/Data/Application/47A5A8B9-65D4-46B3-9021-88A0A931B729/tmp/.com.apple.Foundation.NSItemProvider.JaDS0I/8AA988EC-482E-4431-8801-5354C96637E8.wav

Here’s how I read it:

    juce::URL fileURL = juce::URL(files[0]);

    auto options = juce::URL::InputStreamOptions(juce::URL::ParameterHandling::inAddress);
    
    reader.reset(mFormatManager.createReaderFor(pathURL.createInputStream(options)));

But it got me error in FileInputStream::read after I dropped the file, saying it’s not openedOK.

int FileInputStream::read (void* buffer, int bytesToRead)
{
    // You should always check that a stream opened successfully before using it!
    jassert (openedOk());

    // The buffer should never be null, and a negative size is probably a
    // sign that something is broken!
    jassert (buffer != nullptr && bytesToRead >= 0);

    auto num = readInternal (buffer, (size_t) bytesToRead);
    currentPosition += (int64) num;

    return (int) num;
}

Is there specific requirement for read a URL like this? Or anything I did was wrong?

Exactly. As you can see the URL path you get is temporary and doesn’t look like normal file path with file name, so once you pass it asynchronously to Juce side it becomes invalid. That’s why I ended up with the following procedure:

  • Access temp URL passed to performDrop native callback using [fileCoordinator coordinateReadingItemAtURL]
  • Ask for permission to generate stable bookmark for this URL [url startAccessingSecurityScopedResource];
  • Generate bookmark and call [bookmark retain] to keep this bookmark in memory so it is not destroyed by ARC (once it is destroyed you lose access to the file)
  • Pass the bookmark to Juce methods for further processing

I’m not sure if this is correct algo, but I’ve played with it around week and all of sudden it started to work exactly this way, and I couldn’t simplify it. Take a look at the native implementation of juce::FileChooser. It has similar steps between getting URL from system and passing URL with bookmark to your async callback.

Hi, I just tried to add the methods for restoring the URL from saved bookmark strings, but got this error while compiling in createFromBookmark():

Use of undeclared identifier 'updateStaleBookmark'

I found this function came from JUCE iOSFileStreamWrapper class, and need to be used like this:

void updateStaleBookmark (NSURL* nsURL, URL& juceUrl)

Do I missed something?

As I said, I have my own class to handle URLs with bookmarks, and I can’t share it completely as it has a lot of dependencies and specific stuff. You can easily make a static method in your class that takes 2 parameters (NSURL* nsURL, URL& juceURL).

Here’s a method I’m using. I think it’s a bit advanced comparing to Juce implementation, but I’m not sure if we need to call startAccessingSecurityScopedResource in this case.

bool MyURL::updateStaleBookmark (void* nsURL)
{
    NSError* error = nil;

    NSURL* const url = static_cast<NSURL *>(nsURL);
    bool securityAccessSucceeded = [url startAccessingSecurityScopedResource];
    
    NSData* const nsBookmark = [url bookmarkDataWithOptions: NSURLBookmarkCreationSuitableForBookmarkFile
                             includingResourceValuesForKeys: @[]
                                              relativeToURL: nil
                                                      error: &error];

    if (securityAccessSucceeded)
        [url stopAccessingSecurityScopedResource];
    
    if (error == nil)
    {
        [nsBookmark retain];
        setURLBookmark (*this, (void *)nsBookmark);
    }
    else
    {
        [[maybe_unused]] auto desc = [error localizedDescription];
        jassertfalse;
        return false;
    }
    return true;
}

The bookmark becomes stale if the file pointed by bookmark has been rewritten, so you can come up with some test cases for that method.

The thread is was awesome and really helped me get drag and drop happening on iOS!

Has anyone also been able to implement performExternalDragDropOfFiles to drag files from a Component into the outside world?