You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于NSFilePromiseProvider的文件拖拽至iTerm/TextEdit路径插入失败排查

问题描述

我开发了一个展示文件的表格视图,希望能将1行或多行拖拽至iTerm或TextEdit中,以文件路径字符串形式插入。我使用现代的NSFilePromiseProvider实现该功能,参照苹果官方《Support Image Export by Providing File Promises》示例编写了FilePromiseProvider类:

class FilePromiseProvider: NSFilePromiseProvider {
    
    struct UserInfoKeys {
        static let url = "url"
    }
    
    override func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
        var types = super.writableTypes(for: pasteboard)

        types.append(contentsOf: [.fileURL, .string]) // <-- Added .string
        return types
    }
    
    override func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? {
        guard let userInfoDict = userInfo as? [String: Any] else { return nil }
    
        switch type {
        case .fileURL:
            // Incoming type is "public.file-url", return (from our userInfo) the URL.
            if let url = userInfoDict[FilePromiseProvider.UserInfoKeys.url] as? NSURL {
                return url.pasteboardPropertyList(forType: type)
            }

        case .string:
            if let url = userInfoDict[FilePromiseProvider.UserInfoKeys.url] as? NSURL {
                return url.path
            }
        default: break
        }
        
        return super.pasteboardPropertyList(forType: type)
    }
    
    public override func writingOptions(forType type: NSPasteboard.PasteboardType, pasteboard: NSPasteboard)
        -> NSPasteboard.WritingOptions {
        return super.writingOptions(forType: type, pasteboard: pasteboard)
    }

}

此外,我已将其与表格视图完成绑定:

class ResultsViewController: NSTableViewDelegate {
    // ... etc


    // Queue used for reading and writing file promises.
    var filePromiseQueue: OperationQueue = {
        let queue = OperationQueue()
        return queue
    }()

    func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? {
        var provider: FilePromiseProvider

        if let assets = filesArrayController.arrangedObjects as? [MyFile] {
            let asset = assets[row]
            let ext = asset.url.pathExtension
            
            if #available(macOS 11.0, *) {
                let typeIdentifier = UTType(filenameExtension: ext)
                provider = FilePromiseProvider(fileType: typeIdentifier!.identifier, delegate: self)
                provider.userInfo = [FilePromiseProvider.UserInfoKeys.url: asset.url as Any]
            } else {
                let typeIdentifier =
                      UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext as CFString, nil)
                provider = FilePromiseProvider(fileType: typeIdentifier!.takeRetainedValue() as String, delegate: self)
            }
            return provider
        }

        return nil
    }
}


extension ResultsViewController: NSFilePromiseProviderDelegate {
    func operationQueue(for filePromiseProvider: NSFilePromiseProvider) -> OperationQueue {
        return filePromiseQueue
    }

    // Never called. Why not?
    func filePromiseProvider(_ provider: NSFilePromiseProvider, fileNameForType fileType: String) -> String {
        let asset = assetFromFilePromiseProvider(provider: provider)
        let fileName = (asset?.url.lastPathComponent)!
        return (asset?.url.lastPathComponent)!
    }
    
    func filePromiseProvider(_ provider: NSFilePromiseProvider,
                                         writePromiseTo url: URL,
                                         completionHandler: @escaping (Error?) -> Void) {
        do {
            if let asset = assetFromFilePromiseProvider(provider: provider) {
                /** Copy the file to the location provided to us. We always do a copy, not a move.
                 It's important you call the completion handler.
                 */
                try FileManager.default.copyItem(at: asset.url, to: url)
            }
            completionHandler(nil)
        } catch let error {
            OperationQueue.main.addOperation {
                self.presentError(error, modalFor: self.view.window!,
                                  delegate: nil, didPresent: nil, contextInfo: nil)
            }
            completionHandler(error)
        }
    }
    
    func assetFromFilePromiseProvider(provider: NSFilePromiseProvider) -> MyFile? {
        var returnAsset: MyFile?
        
        if  let userInfo = provider.userInfo as? [String: Any],
            let row = userInfo[FilePromiseProvider.UserInfoKeys.rowNumber] as? Int {
            if let assets = filesArrayController.arrangedObjects as? [MyFile] {
                returnAsset = assets[row]
            }
        }
        return returnAsset
    }
}

目前功能在部分场景正常:

  • 拖拽至Finder可完成文件复制
  • 拖拽至打开/保存对话框可切换文件夹或文件名
  • 拖拽至Terminal.app可插入文件路径

但拖拽至TextEdit或iTerm 2时无任何效果,目标窗口无变化。而从Finder或Panic的Transmit拖拽文件至这些应用却能正常插入路径,说明我的实现存在问题。

我通过Pasteboard Viewer查看了拖拽项的类型标识符:

类型标识符值
com.apple.NSFilePromiseItemMetaData二进制plist,显示public.jpeg
com.apple.pasteboard.promised-file-name空
com.apple.pasteboard.promised-suggested-file-name空
com.apple.pasteboard.promised-file-content-typepublic.jpeg
com.apple.pasteboard.NSFilePromiseID每个拖拽项唯一
public.utf8-plain-text示例:/path/to/file name.jpg
public.file-url示例:file:///path/to/file%20name.jpg
dyn.xxxxx两个此类类型,分别为56字节和0字节,未知用途
com.apple.pasteboard.promised-file-url空

我还发现:

  • filePromiseProvider(_:fileNameForType:)方法从未被调用
  • promised-file-name类型值为空(但Panic的实现中该值也为空,可能属正常情况)
  • 拖拽时未显示正确的复制指针(Finder拖拽也无此指针但功能正常,Transmit拖拽时会显示绿色复制指针)
  • 我在tableView(:validateDrop:proposedRow:proposedDropOperation:)中返回了.copy,但该代理方法仅在拖放目标为表格自身时才会被调用

请问我哪里出错了?


问题排查与修复方案

1. 核心逻辑错误:assetFromFilePromiseProvider无法正确获取文件对象

你在FilePromiseProvider的userInfo中存储的是url,但在assetFromFilePromiseProvider里却尝试读取不存在的rowNumber,这会导致所有依赖该方法的逻辑失效,包括fileNameForType和文件复制操作。

修复:
修改tableView(_:pasteboardWriterForRow:)中设置userInfo的代码,同时存入行号:

provider.userInfo = [
    FilePromiseProvider.UserInfoKeys.url: asset.url as Any,
    "rowNumber": row // 新增行号存储
]

2. 未支持多行拖拽

当前实现仅处理单行拖拽,当表格选中多行时,需要返回包含多个NSFilePromiseProvider的数组,而非单个provider。

修复:
更新tableView(_:pasteboardWriterForRow:)方法,支持多选行:

func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? {
    guard let assets = filesArrayController.arrangedObjects as? [MyFile] else {
        return nil
    }
    
    let selectedRows = tableView.selectedRows
    // 处理多行拖拽
    if selectedRows.count > 1 {
        let providers = selectedRows.map { row -> FilePromiseProvider in
            let asset = assets[row]
            let ext = asset.url.pathExtension
            let typeIdentifier: String
            if #available(macOS 11.0, *) {
                typeIdentifier = UTType(filenameExtension: ext)!.identifier
            } else {
                let utType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext as CFString, nil)
                typeIdentifier = utType!.takeRetainedValue() as String
            }
            let provider = FilePromiseProvider(fileType: typeIdentifier, delegate: self)
            provider.userInfo = [
                FilePromiseProvider.UserInfoKeys.url: asset.url as Any,
                "rowNumber": row
            ]
            return provider
        }
        return NSArray(array: providers) as NSPasteboardWriting
    } 
    // 单行拖拽逻辑
    else {
        let asset = assets[row]
        let ext = asset.url.pathExtension
        let typeIdentifier: String
        if #available(macOS 11.0, *) {
            typeIdentifier = UTType(filenameExtension: ext)!.identifier
        } else {
            let utType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext as CFString, nil)
            typeIdentifier = utType!.takeRetainedValue() as String
        }
        let provider = FilePromiseProvider(fileType: typeIdentifier, delegate: self)
        provider.userInfo = [
            FilePromiseProvider.UserInfoKeys.url: asset.url as Any,
            "rowNumber": row
        ]
        return provider
    }
}

3. 拖拽类型优先级问题

TextEdit和iTerm会优先读取public.utf8-plain-text或public.file-url,但你当前是将这两个类型追加到默认类型列表末尾,目标应用可能优先读取文件promise类型而非路径字符串。

修复:
调整writableTypes中类型的顺序,将字符串和文件URL类型移到开头:

override func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
    var types = super.writableTypes(for: pasteboard)
    // 将字符串和文件URL类型放在最前面,确保目标应用优先读取
    types.insert(contentsOf: [.string, .fileURL], at: 0)
    return types
}

4. 拖拽指针显示问题

表格视图作为拖拽源时,需要通过专门的代理方法设置操作掩码,而非仅在内部拖放时设置。

修复:
添加拖拽会话操作掩码设置方法:

func tableView(_ tableView: NSTableView, draggingSession session: NSDraggingSession, sourceOperationMaskForDraggingContext context: NSDraggingContext) -> NSDragOperation {
    return .copy // 明确设置为复制操作,部分应用会显示对应的复制指针
}

关于fileNameForType未被调用的说明

该方法仅在目标应用主动请求文件promise的文件名时才会触发,TextEdit/iTerm这类应用直接读取了路径字符串类型,因此不会调用这个方法。修复上述逻辑后,若有应用需要该方法,也能正确返回结果。

内容的提问来源于stack exchange,提问作者Nick K9

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 07:57:01