Home   

How to Actually Implement File Dragging From Your App on Mac

In macOS 10.12, Apple introduced a new API for dragging files, NSFilePromiseProvider. As far as I can tell, the only documentation explaining how to use NSFilePromiseProvider comes from the 2016 What’s New in Cocoa WWDC Video. As a result, when developers search for this information, they’re likely to only find outdated documentation on Apple’s site. This article explains how to implement file dragging using the latest APIs. Hopefully it will help someone searching for this information.

How to Create an NSFilePromiseProvider

When creating an NSFilePromiseProvider object, you must supply a UTI which conforms to public.data or public.directory and implement its required delegate methods. Apple’s documentation contains a list of built-in UTI, and you can also specify your own in your app’s Info.plist.

The first delegate method you need to implement, filePromiseProvider(_:fileNameForType:)-filePromiseProvider:fileNameForType: is called before the drag has completed. You should return the base filename form this method. At this point, you do not know the what the full path of the file will be.

The second delegate method you need to implement, filePromiseProvider(_:writePromiseTo:completionHandler:)-filePromiseProvider:writePromiseToURL:completionHandler: is called as the drag finishes. The URL to write your file to is passed in. This is the full path, including the filename returned from the first delegate method. You should perform file writing in this method, and then call the completion handler.

Here is a basic example of these delegate methods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyDelegate {
    public func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider,
                                    fileNameForType fileType: String)
        -> String {

        return "sample.txt";
    }

    public func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider,
                                    writePromiseTo url: URL,
                                    completionHandler: @escaping (Error?)
        -> Void) {

        do {
            try "Hello world!".write(to: url, atomically: true, encoding: .utf8)
            completionHandler(nil)
        } catch let error {
            completionHandler(error)
        }
    }
}

What to do With an NSFilePromiseProvider

When you create your NSFilePromiseProvider object, and what you do with it, depends on what kind of view you are dragging from.

Since NSFilePromiseProvider conforms to NSPasteboardWriting, you can use it in most places that you can use an NSPasteboardItem for dragging. However, even though it confirms to NSPasteboardWriting if you write your NSFilePromiseProvider directly to the pasteboard, it will not work properly.

From an NSTableView

You should return your NSFilePromiseProvider from the NSTableViewDataSource method tableView(_:pasteboardWriterForRow:)-tableView:pasteboardWriterForRow:. If you’re using an NSOutlineView, you should use the corresponding method from NSOutlineViewDataSource.

From an NSCollectionView

You should return your NSFilePromiseProvider from the NSCollectionViewDelegate method collectionView(_:pasteboardWriterForItemAt:)- (id<NSPasteboardWriting>)collectionView:(NSCollectionView *)collectionView pasteboardWriterForItemAtIndexPath:(NSIndexPath *)indexPath;.

From Any Other NSView

You should initialize an NSDraggingItem with your NSFilePromiseProvider, then pass that NSDraggingItem to a call to beginDraggingSession(with:event:source:)-beginDraggingSessionWithItems:event:source:. You can invoke this method in mouseDown(with:)-mouseDown: or mouseDragged(with:)-mouseDragged:. For more information, see the documentation for NSDraggingSession.

Combining With Other Drag Types

In general, to adopt this new API, you replace NSPasteboardItems that drag file promises with NSFilePromiseProvider. However, NSPasteboardItem allows you to drag multiple types of objects in a single NSDraggingItem, while NSFilePromiseProvider only allows you to drag files. Dragging multiple kinds of files can be useful to, for example, allow drags between panes in your app to pass around objects in memory, but allow drags from your app to the Finder to drag files.

To achieve the same with NSFilePromiseProvider, you’ll need to subclass it and override a few methods.

First, you’ll need to override the writableTypes(for:)-writableTypesForPasteboard: method and return all the drag types your drag supports. You can call the superclass method to get an array of file types you’re dragging, and then add the non-file types to that array and return it.

Next, you’ll need to override the writingOptions(forType:pasteboard:)-writingOptionsForType:pasteboard: method and return []0 for your non-file drag types. For file drag types, you should return the superclass method.

Finally, you’ll need to implement the pasteboardPropertyList(forType:)-pasteboardPropertyListForType: method and return the data you want written to the pasteboard. For file drag types, you’ll need to return the superclass method.

Here is a basic example of this technique.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
let myType = NSPasteboard.PasteboardType("CustomType")

public class MyFilePromiseProvider : NSFilePromiseProvider {
    public override func writableTypes(for pasteboard: NSPasteboard)
        -> [NSPasteboard.PasteboardType] {

        var types = super.writableTypes(for: pasteboard)
        types.append(myType)

        return types;
    }

    public override func writingOptions(forType type: NSPasteboard.PasteboardType,
                                        pasteboard: NSPasteboard)
        -> NSPasteboard.WritingOptions {

        if type == myType {
            return []
        }

        return super.writingOptions(forType: type, pasteboard: pasteboard)
    }

    public override func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType)
        -> Any? {

        if type == myType {
            return "Hello world!"
        }

        return super.pasteboardPropertyList(forType: type)
    }
}