肇鑫的技术博客

业精于勤,荒于嬉

获得Finder拖动来的文件URL

新项目需要获得从Finder拖动过来的文件URL。这篇文档是一篇总结。

根据Drag and Drop Programming Topics,拖放操作有拖动源和拖动目的地。Finder是源,我的App是目的地。

拖动的目的地必须是NSWindowNSView及其子类,我选择ViewController对应的view作为源。

注册UTI文件类型

首先要注册view可以从剪贴板获得的数据类型。由于历史的原因,这部分实际上比较混乱。

// MARK: - Dragging destination
required init?(coder: NSCoder) {
    super.init(coder: coder)
    
    // Dragging destination
    self.register(forDraggedTypes: ["public.file-url", "public.folder"]) // somehow, public.folder isn't used
}

避免使用剪贴板类型

虽然苹果的指南里使用的还是NSFilenamesPboardType,但是实际上在API文档里,这种方式已经是不建议使用的了。

Version-Notes
Pboard types will be deprecated in a future release. In macOS 10.6 and later you should replace any use of pboard types with UTIs, including the constants described in Types for Standard Data (macOS 10.6 and later).
版本注释
剪贴板类型在未来的版本会被反对。在macOS 10.6之后的版本你应该使用UTI替代剪贴板类型,即使是10.6引入的新版的也同样替代。
Types for Standard Data (OS X v10.5 and earlier)

UTI文件类型浅析

那么什么是UTI呢?其实就是苹果使用的文件类型系统。具体的可以看这个表格

几种类型的区别

比如我使用的是["public.file-url", "public.folder"],你可以在上面提到的表格里查到这两个常量。你也可以使用苹果定义的常量kUTTypeFileURLkUTTypeFolder作为替代,但是这两个常量是CFString的格式,所以使用时需要转换为String才可以。

由于历史的原因,苹果为了保证兼容性,在剪贴板的类型里会显示多个类型。比如foo.txt,从Finder拖动时,剪贴板里的类型是这样的。

public.file-url
CorePasteboardFlavorType 0x6675726C
dyn.ah62d4rv4gu8y6y4grf0gn5xbrzw1gydcr7u1e3cytf2gn
NSFilenamesPboardType
dyn.ah62d4rv4gu8yc6durvwwaznwmuuha2pxsvw0e55bsmwca7d3sbwu
Apple URL pasteboard type
com.apple.finder.node

可以看到,”public.file-url“和”Apple URL pasteboard type“是同时存在的。kUTTypeFileURL的值实际上对应的就是"public.file-url"。

(lldb) print kUTTypeFileURL
(CFString) $R0 = 0x00007fffc41bc6d0 {}
(lldb) print kUTTypeFileURL as String
(String) $R1 = "public.file-url"
(lldb) print NSFilenamesPboardType
(String) $R2 = "NSFilenamesPboardType"

拖动到目标时

override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
    let sourceDragMask = sender.draggingSourceOperationMask()
    let pb = sender.draggingPasteboard()
    
    if pb.types?.contains("public.file-url") == true {
        if sourceDragMask.contains(.generic) {
            return .generic
        }
    }
    
    return [] // NSDragOperationNone
}

获得剪切板的数据

override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
    let pb = sender.draggingPasteboard()
    
    if pb.types?.contains("public.file-url") == true {
        let urls = pb.readObjects(forClasses: [NSURL.self]) as! [URL]
        
        if let controller = nextResponder as? ViewController {
            controller.urls = urls
            
            return true
        }
    }
    
    return false
}

NSURL与URL

这里必须使用NSURL而不能使用URL,因为前者实现了剪切板读取协议,而后者没有。

剪切板读取协议

NSPasteboardReading协议。

以下类实现了该协议。由于这个协议继承至NSObjectProtocol,只有NSObjectNSProxy的子类才能实现该协议。
NSAttributedString
NSColor
NSFilePromiseReceiver
NSImage
NSPasteboardItem
NSSound
NSString
NSTextStorage
NSURL