swiftmacosnspasteboardnsdragginginfonsdraggingitem

NSPasteboard Copy Dragged and Dropped File


Although I have working code, I'm confused about whether I'm taking the right approach to NSPasteboard and I am reaching out to see if anyone can help me clarify the best approach.

There doesn't appear to be any full documentation written by Apple about dragging and dropping since 2012 and in that time there seems to have been deprecation and a change in the way (especially with sandboxed apps) in which you are supposed to implement it. After much messing around I discovered that the file URL supplied by the dragged pasteboard reveals its true path (as opposed to the anonymised string of numbers which make up the url) when you access its path property and this enables a regular copy to be made using FileManager.

My question is, am I following the right procedure here? Casting the NSURL obtained from the pasteboard to a URL type and using it in copyItem(at:URL, to:URL) doesn't work (even though copying and pasting the same url into Safari enables me to view the file). Hence I use instead copyItem(atPath:String, toPath:String). To further labour this point, is there a method for the URL type that replaces NSURL(from:NSPasteboard)? It seems like there should be.

Anyway here's the code I have working (please ignore the code inside draggingEntered: for now, it just gets things working for testing, and the fact that I register for NSFilenamesPboardType but then ignore it can also be overlooked also).

import Cocoa

class DraggerView: NSView {
    let types = [NSURLPboardType, NSFilenamesPboardType]
    var directory:URL!

    override init(frame: NSRect) {
        super.init(frame: frame)
        register(forDraggedTypes: types)
        if  let dir = URL(string:NSTemporaryDirectory())
        {
            directory = dir
        }
    }


    required init?(coder: NSCoder) {
        super.init(coder: coder)
        register(forDraggedTypes: types)
        if  let dir = URL(string:NSTemporaryDirectory())
        {
            directory = dir
        }
    }

    override func draw(_ dirtyRect: NSRect)  {
        super.draw(dirtyRect)
        NSColor.white.set()
        NSRectFill(dirtyRect)
    }

    override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation  {
        return NSDragOperation.copy
    }


    override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {

        let pboard = sender.draggingPasteboard()
        if let types = pboard.types, types.contains(NSURLPboardType) {


            // add file name to save to loaction
            if let fileURL = NSURL(from:pboard), let filename = fileURL.lastPathComponent {
                directory.appendPathComponent(filename)
                do {
                    try FileManager.default.copyItem(atPath:fileURL.path!, toPath:directory.path)
                }
                catch {
                    // something went wrong
                }
            }


            return true
        }
        else { return false }
    }
}

One final point is that the docs keep referring to using UTIs when registering for dragged types, but I'm not clear how far this extends. Should I replace NSURLPboardType and NSFilenamesPboardType with UTIs?


Solution

  • Following advice on the Apple developer forums, I was able to ditch a lot of the code and implement the readObjects: method. This simplifies things greatly.

    I now have the following code working (by using the preferred method of URLs not paths).

    import Cocoa
    
    class DraggerView: NSView {
    
        override init(frame: NSRect) {
            super.init(frame: frame)
            self.register(forDraggedTypes: [NSURLPboardType])
        }
    
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            self.register(forDraggedTypes: [NSURLPboardType])
        }
    
        override func draw(_ dirtyRect: NSRect)  {
            super.draw(dirtyRect)
            NSColor.white.set()
            NSRectFill(dirtyRect)
        }
    
    
    
        override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation  {
           return NSDragOperation.copy
        }
    
    
        override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
            let pboard = sender.draggingPasteboard()
            if let urls = pboard.readObjects(forClasses: [NSURL.self], options: [:]) as? [URL] {
                for url in urls {
                    do {
                        var directory = URL(fileURLWithPath:NSTemporaryDirectory())
                        directory.appendPathComponent(url.lastPathComponent)
                        try FileManager.default.copyItem(at:url, to:directory)
                    }
                    catch {
                        // something went wrong
                    }
    
                }
            }
    
    
            return true
        }
    }
    

    Note: The above code copies any dragged and dropped file(s) or folder(s) (incl. files and subfolders) to your app's temporary directory.