swiftswiftuidrag-and-dropnspasteboardnsitemprovider

NSItemProviderWriting for dragging content to Finder


I'm building a SwiftUI view with draggable content, with a custom NSItemProviderWriting class to provide one type of data for when dragged into another part of the app, and another type of data when dragged into Finder. I'm having the hardest time setting up the NSItemProviderWriting in order to drag to the Finder, though.

First, here's the pertinent part of the view:

myView
    .onDrag {
        NSItemProvider(object: MyDragProvider("dragged text"))
    }

Simple enough. MyDragProvider is where the work needs to get done for now:

class TestDragProvider: NSObject, NSItemProviderWriting {
    var stringValue: String

    init(_ value: String) {
        self.stringValue = value
    }

    static var writableTypeIdentifiersForItemProvider: [String] {
        let utTypes: [UTType] = [
            myCustomUTType,
            .fileUrl
        ]
        return utTypes.map { $0.identifier }
    }

    func loadData(
        withTypeIdentifier typeIdentifier: String,
        forItemProviderCompletionHandler completionHandler: @escaping @Sendable (Data?, Error?) -> Void
    ) -> Progress? {
        let progress = Progress(totalUnitCount: 1)
        var data: Data? = nil
        
        print("Loading data for type: \(typeIdentifier)")
        if let utType = UTType(typeIdentifier) {
            if utType == myCustomUTType {
                data = stringValue.data(using: .utf8)
            } else if utType == .fileUrl {
                let urlOfSomeFile = FileManager
                                        .default
                                        .urls(for: .cachesDirectory, in: .userDomainMask)[0]
                                        .appendingPathComponent("fileToDrag.txt")
                                        .absoluteURL
                data = try! NSKeyedArchiver.archivedData(withRootObject: (urlOfSomeFile as NSURL), requiringSecureCoding: false)
            }
        }

        progress.completedUnitCount = data != nil ? 1 : 0
        completionHandler(data, nil)
        return progress
}

This works fine when dragging items into the other part of the app that receives drags of this type (the print statement outputs Loading data for type: myCustomUTType), but when I try to drag outside of the app, the console puts out this:

NSURLs written to the pasteboard via NSPasteboardWriting must be absolute URLs. NSURL 'bplist00%C3%94%01%02%03%04%05%06%07%0AX$versionY$archiverT$topX$objects%12%00%01%C2%86%C2%A0_%10%0FNSKeyedArchiver%C3%91 ... 00%01%1B' is not an absolute URL.

[sandbox] CreateSandboxExtensionData failed: urlData: 0x600000be9640 length: 0 (-1 means null)

[general] Sandbox extension data required immediately for flavor public.file-url, but failed to obtain.

[sandbox] CreateSandboxExtensionData failed: urlData: 0x600000be9640 length: 0 (-1 means null)

[sandbox] Failed to get a sandbox extension

I guess the [sandbox] and [general] errors are about permissions, but the first error about absolute URLs is confounding. I've tried several variations for that, including adding or removing the .absoluteURL on the urlOfSomeFile, and switching the NSKeyedArchiver to a JSONEnconder.

Anybody else know how to get past this?


Solution

  • Have you tried asking the URL for its dataRepresentation?

    data = urlOfSomeFile.dataRepresentation
    

    I tested the following Xcode Playground and dragging to the Finder does what I expect (moves fileToDrag.txt to the desktop):

    import SwiftUI
    import UniformTypeIdentifiers
    
    class MyDragProvider: NSObject, NSItemProviderWriting {
      static var writableTypeIdentifiersForItemProvider: [String] {
        return [UTType.fileURL].map(\.identifier)
      }
    
      func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
        switch typeIdentifier {
        case UTType.fileURL.identifier:
          let urlOfSomeFile = FileManager
            .default
            .urls(for: .cachesDirectory, in: .userDomainMask)[0]
            .appendingPathComponent("fileToDrag.txt")
            .absoluteURL
          try! String("hello world").write(to: urlOfSomeFile, atomically: true, encoding: .utf8)
          let data = urlOfSomeFile.dataRepresentation
          completionHandler(data, nil)
          return nil
        default:
          fatalError()
        }
      }
    }
    
    import PlaygroundSupport
    PlaygroundPage.current.setLiveView(
      Text("Drag me")
        .padding()
        .onDrag {
          NSItemProvider(object: MyDragProvider())
        }
    )