swiftdrag-and-dropuikitmac-catalystnsitemprovider

How to drag a table view item to Finder to create a folder in Catalyst App


I have a UITableView that displays a Group/Note hierarchy analogous to a Finder Folder/Files hierarchy. I have implemented Drag and Drop such that if I drag a Note to the Finder Desktop an HTML file is created of the Note, and if I drag a Group to the Finder Desktop a Text file is created of the Group's title. Here is the Helper Dragging extension that I've implemented:

//
//  Model+Dragging.swift
//

/*
Abstract:
 
 Helper methods for providing and consuming drag-and-drop data.
*/

import UIKit
import MobileCoreServices

// Conditionalize Drag and Drop so it is not in iOS target.
#if targetEnvironment(macCatalyst)

extension Model {
    /**
         A helper function that serves as an interface to the data model,
         called by the implementation of the `tableView(_ canHandle:)` method.
    */
    func canHandle(_ session: UIDropSession) -> Bool {
        // In order to enable dragging all text type files changed the class
        //  loadable objects from NSString to my custom TEXTClass.
        return session.canLoadObjects(ofClass: TEXTClass.self)
    }
    
    /**
         A helper function that serves as an interface to the data model, called
         by the `tableView(_:itemsForBeginning:at:)` method.
    */
    func dragItems(for indexPath: IndexPath) -> [UIDragItem] {
        let itemProvider = NSItemProvider()
        let item = self.getDisplayItem(for: indexPath.row)
        if item is Note {
            let note:Note = item as! Note
            let html = note.noteHTML
            let data = html?.data(using: .utf8)

            // Drag to finder creates an html file.
            itemProvider.suggestedName = note.title + ".html"
            itemProvider.registerDataRepresentation(forTypeIdentifier: kUTTypeData as String, 
                                                    visibility: .all) { completion in
                completion(data, nil)
                return nil
            }
        } else {
            let group:Group = item as! Group
            let title = group.title 
            let data = title?.data(using: .utf8)

            // Drag to finder creates a text file.
            itemProvider.suggestedName = group.title + ".text"
            itemProvider.registerDataRepresentation(forTypeIdentifier: kUTTypeData as String, 
                                                    visibility: .all) { completion in
                completion(data, nil)
                return nil
            }
        }

        return [
            UIDragItem(itemProvider: itemProvider)
        ]
    }
}

#endif

I would now like to change the result of dragging a Group to the Finder. Instead of creating a Text file from the drag data, I would like to create a Folder containing the Group's Notes.

Note: I am unconcerned with the task of assembling the Group/Notes hierarchies as Folder/Files hierarchies because I already have implemented such previously as a menu export command. My concern is where and how I can communicate it to the Finder in the Drag and Drop process.

As a first step I thought I would simply create an empty folder from the drag of a Group. So far, none of my experiments have been successful. Every variation of itemProvider.registerDataRepresentation(forTypeIdentifier: or itemProvider.registerFileRepresentation(forTypeIdentifier: or registerItem(forTypeIdentifier:loadHandler: that I have tried has failed to produce anything but empty TEXT files, if they worked at all.

It is my understanding that itemProviders may provide directories instead of files, but I have been unable to find any examples of such.

My problem may be a lack of understanding of the syntax and usage of the NSItemProvider.LoadHandler completion block.

Any Swift examples on point would be greatly appreciated!


Solution

  • I was able to craft a solution with the help of this answer to another NSItemProvider stackoverflow.com question.

    The solution uses a loadHandler to build the directory tree in registerDataRepresentation:

    extension Model {
        …
        …
        …
    /**
         A helper function that serves as an interface to the data model, called
         by the `tableView(_:itemsForBeginning:at:)` method.
    */
    // Implement Menu command "ExportGroup as HTML…" as a Drag and Drop feature.//leg20240724 - TopXNotes_Catalyst_v3.0.0
    func dragItems(for indexPath: IndexPath) -> [UIDragItem] {
        let itemProvider = NSItemProvider()
    
        
        if item is Note {
            let data = …
    
            // Drag to Finder creates an html file.
            itemProvider.suggestedName = …
            itemProvider.registerDataRepresentation(forTypeIdentifier: kUTTypeData as String, visibility: .all) { completion in
                completion(data, nil)
                return nil
            }
        } else {
            // Drag to Finder creates a folder tree.
            itemProvider.registerDataRepresentation(forTypeIdentifier: kUTTypeFileURL as String, visibility: .all, loadHandler: loadFileForItemProvider)
        }
    
        return [
            UIDragItem(itemProvider: itemProvider)
        ]
    }
    
    // Retrieve the Group tree from the tableView and assemble the
    //  corresponding Directory tree for the Finder.
    //
    func loadFileForItemProvider(onComplete callback: @escaping (Data?, Error?) -> Void) -> Progress? {
        let progress = Progress(totalUnitCount: 100)
    
        
        // Build the Group directory tree in a temporary location and
        //  provide the URL in the callback.
        let groupURL =  …
        
        Task.detached {
            do {
                // Drop directory tree on the Finder.
                callback(groupURL?.dataRepresentation, nil)
                    
                progress.completedUnitCount = 100
            }
        }
    
        return nil
    }
        
    }