How to create NSPasteboardWriting for Drag and Drop in NSCollectionView
Asked Answered
T

2

6

I have a one-section collection view and would like to implement Drag and Drop to allow reordering of the items. The CollectionViewItem has several textviews showing properties form my Parameter objects. Reading the doc I need to implement the NSCollectionView delegate:

func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
    let parameter = parameterForIndexPath(indexPath: indexPath)
    return parameter // throws an error "Cannot convert return expression of type 'Parameter' to return type 'NSPasteboardWriting?'"
}

I have not found any information understandable for me describing the nature of the NSPasteboardWriting object. So, I have no idea how to proceed... What is the NSPasteboardWriting object and what do I need to write in the pasteboard?

Thanks!

Thanatopsis answered 18/2, 2017 at 12:44 Comment(0)
C
7

Disclaimer: I have struggled to find anything out there explaining this in a way that made sense to me, especially for Swift, and have had to piece the following together with a great deal of difficulty. If you know better, please tell me and I will correct it!

The "pasteboardwriter" methods (such as the one in your question) must return something identifiable for the item about to be dragged, that can be written to a pasteboard. The drag and drop methods then pass around this pasteboard item.

Most examples I've seen simply use a string representation of the object. You need this so that in the acceptDrop method you can get your hands back on the originating object (the item being dragged). Then you can re-order that item's position, or whatever action you need to take with it.

Drag and drop involves four principal steps. I'm currently doing this with a sourcelist view, so I will use that example instead of your collection view.

  1. in viewDidLoad() register the sourcelist view to accept dropped objects. Do this by telling it which pasteboard type(s) it should accept.

    // Register for the dropped object types we can accept. sourceList.register(forDraggedTypes: [REORDER_SOURCELIST_PASTEBOARD_TYPE])

    Here I'm using a custom type, REORDER_SOURCELIST_PASTEBOARD_TYPE that I define as a constant like so:

       `let REORDER_SOURCELIST_PASTEBOARD_TYPE = "com.yourdomain.sourcelist.item"`
    

    ...where the value is something unique to your app ie yourdomain should be changed to something specific to your app eg com.myapp.sourcelist.item.

    I define this outside any class (so it can be accessed from several classes) like so:

       import Cocoa
    
       let REORDER_SOURCELIST_PASTEBOARD_TYPE = "com.yourdomain.sourcelist.item"`
    
       class Something {
          // ...etc...
    
  2. implement the view's pasteboardWriterForItem method. This varies slightly depending on the view you're using (i.e. sourcelist, collection view or whatever). For a sourcelist it looks like this:

      // Return a pasteboard writer if this outlineview's item should be able to 
      // drag-and-drop.
      func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? {
        let pbItem = NSPasteboardItem()
    
        // See if the item is of the draggable kind. If so, set the pasteboard item.
        if let draggableThing = ((item as? NSTreeNode)?.representedObject) as? DraggableThing {
              pbItem.setString(draggableThing.uuid, forType: REORDER_SOURCELIST_PASTEBOARD_TYPE)
              return pbItem;
        }
    
        return nil
      }
    

    The most notable part of that is draggableThing.uuid which is simply a string that can uniquely identify the dragged object via its pasteboard.

  3. Figure out if your dragged item(s) can be dropped on the proposed item at the index given, and if so, return the kind of drop that should be.

    func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation {
        // Get the pasteboard types that this dragging item can offer. If none
        // then bail out.
        guard let draggingTypes = info.draggingPasteboard().types else {
            return []
        }
    
        if draggingTypes.contains(REORDER_SOURCELIST_PASTEBOARD_TYPE) {
            if index >= 0 && item != nil {
                return .move
            }
          }
    
          return []
        }
    
  4. Process the drop event. Do things such as moving the dragged item(s) to their new position in the data model and reload the view, or move the rows in the view.

    func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool {
      let pasteboard = info.draggingPasteboard()
    
      let uuid = pasteboard.string(forType: REORDER_SOURCELIST_PASTEBOARD_TYPE)
    
      // Go through each of the tree nodes, to see if this uuid is one of them.
      var sourceNode: NSTreeNode?
      if let item = item as? NSTreeNode, item.children != nil {
        for node in item.children! {
            if let collection = node.representedObject as? Collection {
                if collection.uuid == uuid {
                    sourceNode = node
                }
            }
        }
      }
    
      if sourceNode == nil {
        return false
      }
    
      // Reorder the items.
      let indexArr: [Int] = [1, index]
      let toIndexPath = NSIndexPath(indexes: indexArr, length: 2)
      treeController.move(sourceNode!, to: toIndexPath as IndexPath)
    
      return true
    }
    

Aside: The Cocoa mandate that we use pasteboard items for drag and drop seems very unnecessary to me --- why it can't simply pass around the originating (i.e. dragged) object I don't know! Obviously some drags originate outside the application, but for those that originate inside it, surely passing the object around would save all the hoop-jumping with the pasteboard.

Chopin answered 17/4, 2017 at 22:7 Comment(4)
After a long time of recovering from pain I start trying to solve my still not working drag and drop issue. I do not understand what you mean with let REORDER_SOURCELIST_PASTEBOARD_TYPE = "com.yourdomain.sourcelist.item". What is "yourdomain" and do I need to change it to something that fits to my app? What do you mean with "that I define outside my class..."? What do you mean with outside the class? ThanksThanatopsis
The REORDER_SOURCELIST_PASTEBOARD_TYPE needs to uniquely identify a custom type of pasteboard "thing", which in this case is the pasteboard item you're inventing to carry your drag-and-drop information. So yes, you're correct — change that to something unique to your app.Chopin
By "outside my class" I meant as an app-wide constant. Sorry for the confusion. I've updated my answer to hopefully clarify this.Chopin
collectionView.registerForDraggedTypes - rename in Swift 4Anglim
T
0

The NSPasteboardWriting protocol provides methods that NSPasteboard (well, technically anyone, I guess) can use to generate different representations of an object for transferring around pasteboards, which is an older Apple concept that is used for copy/paste (hence Pasteboard) and, apparently, drag and drop in some cases.

It seems that, basically, a custom implementation of the protocol needs to implement methods that:

The other answer provides a straightforward implementation of this protocol for use within a single app.


But practically?

The Cocoa framework classes NSString, NSAttributedString, NSURL, NSColor, NSSound, NSImage, and NSPasteboardItem implement this protocol.

So if you've got a draggable item that can be completely represented as a URL (or String, or Color, or Sound, or Image, etc), just take the URL you have and cast it to NSPasteboardWriting?:

func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? {
    let url: URL? = someCodeToGetTheURL(for: indexPath) // or NSURL
    return url as NSPasteboardWriting?
}

This is not helpful if you have a complicated type, but I hope it's helpful if you have a collection view of images or some other basic item.

Trinitrophenol answered 30/12, 2020 at 22:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.