swiftmacosswiftuidrag-and-dropfile-handling

Drag & Drop in SwiftUI fails to handle public.data for all file in Mac


I’m working on an app in SwiftUI, in which I want to support drop file functionality to handle any files. While I try implement this, I am trying accept any files dropped into a custom drop area and display their MIME type. I also provide a fallback file chooser NSOpenPanel. This part works.

However, the drop functionality fails when handling any file.

My HandleDrop code:

private func handleDrop(providers: [NSItemProvider]) -> Bool {
    var success = false

    for provider in providers {
        print("Checking provider: \(provider)")

        // Handle public.file-url
        if provider.hasItemConformingToTypeIdentifier("public.file-url") {
            print("Provider conforms to public.file-url")
            provider.loadItem(forTypeIdentifier: "public.file-url", options: nil) { item, error in
                if let url = item as? URL {
                    print("Dropped file URL: \(url)")
                    DispatchQueue.main.async {
                        droppedFileType = "MIME Type: \(url.mimeType ?? "Unknown")"
                    }
                    success = true
                } else {
                    print("Failed to cast item to URL for public.file-url")
                }
            }
        }

        // Handle public.data
        else if provider.hasItemConformingToTypeIdentifier("public.data") {
            print("Provider conforms to public.data")
            provider.loadItem(forTypeIdentifier: "public.data", options: nil) { item, error in
                if let data = item as? Data {
                    let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("pdf")
                    do {
                        try data.write(to: tempURL)
                        print("Saved file to temporary URL: \(tempURL)")
                        DispatchQueue.main.async {
                            droppedFileType = "MIME Type: \(tempURL.mimeType ?? "Unknown")"
                        }
                        success = true
                    } catch {
                        print("Failed to write data to temporary file")
                    }
                } else {
                    print("Failed to decode item as public.data")
                }
            }
        }

        // Unsupported type
        else {
            print("Provider does NOT conform to public.file-url or public.data")
        }
    }

    return success
}

As mentioned above the File Chooser/NSOpenPanel, works perfectly for all file types. The MIME type is detected correctly and displayed. However, Drag & Drop provider while does conforms to public.data, the decoding as Data fails. My Logs show it Failed to decode item as public.data. I tried with file types like .png or .txt which all fails.

My .entitlement:

<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>

Solution

  • With some simple debugging, you can see that the type of item is actually NSURL?, even in the "public.data" branch.

    print(type(of: item!)) // NSURL
    

    This is because loadItem(forTypeIdentifier:) is kind of broken in Swift.

    Since you want a URL anyway, you don't need to write to a temporary file anymore. Just cast item to URL and it's basically the same as the "public.file-url" branch.

    Also consider using loadFileRepresentation, which directly gives you a URL. Or, use the Transferable-based dropDestination modifier like this:

    .dropDestination(for: URL.self) { urls, location in
        print(urls.map(\.mimeType))
        return true
    }
    

    The way that you are modifying and returning success doesn't make much sense and is not concurrency-safe. You should set success to true outside of the completion handlers. Keep in mind that the completion handlers can be executed concurrently with the code in handleDrop, on a different thread.