如何写入 NSPasteboard 并使用相同的指针获取它

How to write to NSPasteboard and get it with the same Pointer

我有一个符合 NSCoding 协议的自定义 class。下面是我如何实现协议的方法。

User.swift

required init?(coder aDecoder: NSCoder) {
    self.name = aDecoder.decodeObject(forKey: #keyPath(name)) as! String
    self.age = aDecoder.decodeInteger(forKey: #keyPath(age))
}

func encode(with aCoder: NSCoder) {
    aCoder.encode(name, forKey: #keyPath(name))
    aCoder.encode(age, forKey: #keyPath(age))
}

我将阅读选项设置为:

static func readingOptions(forType type: String, pasteboard: NSPasteboard) -> NSPasteboardReadingOptions {
    return .asKeyedArchive
}

每当用户在 NSOutlineView 上拖动单元格行时,此对象将被放入 NSDragPboard 中。你可以看到下面的实现:

UserViewController.h

var users: User // Store the users to be used in the outlineView

.
.
.

func outlineView(_ outlineView: NSOutlineView, writeItems items: [Any], to pasteboard: NSPasteboard) -> Bool {
    if let user = items.first as? User {
        pasteboard.clearContents()
        pasteboard.writeObjects([user])
        return true
    }
    return false
}

用户通过释放鼠标按钮完成拖动后,应用程序将获取粘贴板内容,方法是:

func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool {

    let pasteboard = info.draggingPasteboard()

    if let userInPasteboard = pasteboard.readObjects(forClasses: [User.self], options: nil)?.first as? User {
        // We can access the user in pasteboard here
    }
}

但问题是,粘贴板中的数据与 outlineView 中的数据具有不同的内存地址。

如果我们尝试使用 pasteBoard 中的用户在 outlineView 中查找用户,我们找不到它。

for user in self.users {
    if user == userInPasteboard {
        print("We found the user that was placed on pasteboard") // Never executed
    }
}

我可以为用户 class 实施 Equatable 协议。但我认为,如果我们能以某种方式使从粘贴板读取的对象与写入粘贴板的对象具有相同的指针地址,它就可以工作。

以下是我要实现的目标:

有可能实现吗?怎么样?

有多种方法可以解决这个问题。既然你说(在评论中)这只是为了在单个大纲视图中重新排序,我将解释我是如何做到的。

首先,我在我的数据源中添加了一个私有变量来跟踪被拖动的项目:

private var itemsBeingDragged = [MyItem]()

因为我将在拖放时使用它来获取拖动的项目,所以粘贴板上的内容并不重要。我这样实现 outlineView(_:pasteboardWriterForItem:)

func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? {
    guard let node = item as? MyNode else { return nil }
    let pasteboardItem = NSPasteboardItem()
    pasteboardItem.setString(String(describing: node), forType: dragType)
    return pasteboardItem
}

为了设置和清除 itemsBeingDragged,我实现了这些方法:

func outlineView(_ outlineView: NSOutlineView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItems draggedItems: [Any]) {
    itemsBeingDragged = draggedItems.flatMap({ [=12=] as? MyItem })
}

func outlineView(_ outlineView: NSOutlineView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, operation: NSDragOperation) {
    itemsBeingDragged = []
}

最后,我这样实现了outlineView(_:acceptDrop:item:childIndex:)

func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool {
    guard
        (info.draggingSource() as? NSOutlineView) === outlineView,
        let dropTarget = item as? MyItem
        else { return false }

    // Update model using `dropTarget`, `index`, and `itemsBeingDragged`.
    // Update `outlineView` to match.
    return true
}

如果拖动来自另一个应用程序,则 info.draggingSource()nil。如果拖动来自同一个应用程序,则它是提供拖动项的对象。所以我做的第一件事就是确保拖放来自同一个大纲视图。

更新

如果拖动源在另一个应用程序中,则获取指向原始拖动项的指针毫无意义,因为指针在跨进程边界时无效——原始拖动项在您的进程中不存在。跨进程边界拖动项目的唯一有意义的方法是实际提供被拖动项目的序列化表示。

如果拖动源也在您的应用程序中,那么您可以将指针粘贴到粘贴板上,但这样做仍然不是内存安全的。最好做一些事情,比如定义一个从源中获取拖动项目的协议:

protocol MyDragSource {
    var itemsBeingDragged: [Any]?
}

然后,在acceptDrop

guard
    let sameAppSource = info.draggingSource(),
    let source = (sameAppSource as? MyDragSource) ?? (sameAppSource as? NSTableView)?.dataSource as? MyDragSource,
    let draggedItems = source.itemsBeingDragged?.flatMap({ [=15=] as? AcceptableItem }),
    draggedItems.count > 0
    else { return false }

…其中 AcceptableItem 是您用于大纲视图项目的任何类型。

使用 info.draggingSource() 并不丢人。这是有原因的。