拖拽在NSTableView上删除重新排序行

时间:2010-01-23 03:05:44

标签: macos drag-and-drop nstableview

我只是想知道是否有一种简单的方法来设置NSTableView以允许它重新排序其行而无需编写任何粘贴板代码。我只需要它就可以在一个表内部完成。我编写pboard代码没有问题,除了我很确定我看到Interface Builder在某个地方有一个切换/默认情况下看到它工作。这当然似乎是一项非常普遍的任务。

由于

9 个答案:

答案 0 :(得分:27)

将表格视图的数据源设置为符合NSTableViewDataSource的类。

将它放在适当的位置(-applicationWillFinishLaunching-awakeFromNib-viewDidLoad或类似的地方):

tableView.registerForDraggedTypes(["public.data"])

然后实现这三个NSTableViewDataSource方法:

tableView:pasteboardWriterForRow:
tableView:validateDrop:proposedRow:proposedDropOperation:
tableView:acceptDrop:row:dropOperation:

以下是支持拖放重排多行的完整工作代码:

func tableView(tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? {
  let item = NSPasteboardItem()
  item.setString(String(row), forType: "public.data")
  return item
}

func tableView(tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableViewDropOperation) -> NSDragOperation {
  if dropOperation == .Above {
    return .Move
  }
  return .None
}

func tableView(tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableViewDropOperation) -> Bool {
  var oldIndexes = [Int]()
  info.enumerateDraggingItemsWithOptions([], forView: tableView, classes: [NSPasteboardItem.self], searchOptions: [:]) {
    if let str = ($0.0.item as! NSPasteboardItem).stringForType("public.data"), index = Int(str) {
            oldIndexes.append(index)
    }
  }

  var oldIndexOffset = 0
  var newIndexOffset = 0

  // For simplicity, the code below uses `tableView.moveRowAtIndex` to move rows around directly.
  // You may want to move rows in your content array and then call `tableView.reloadData()` instead.
  tableView.beginUpdates()
  for oldIndex in oldIndexes {
    if oldIndex < row {
      tableView.moveRowAtIndex(oldIndex + oldIndexOffset, toIndex: row - 1)
      --oldIndexOffset
    } else {
      tableView.moveRowAtIndex(oldIndex, toIndex: row + newIndexOffset)
      ++newIndexOffset
    }
  }
  tableView.endUpdates()

  return true
}

Swift 3 版本:

func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? {
    let item = NSPasteboardItem()
    item.setString(String(row), forType: "private.table-row")
    return item
}

func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableViewDropOperation) -> NSDragOperation {
    if dropOperation == .above {
        return .move
    }
    return []
}

func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableViewDropOperation) -> Bool {
    var oldIndexes = [Int]()
    info.enumerateDraggingItems(options: [], for: tableView, classes: [NSPasteboardItem.self], searchOptions: [:]) {
        if let str = ($0.0.item as! NSPasteboardItem).string(forType: "private.table-row"), let index = Int(str) {
            oldIndexes.append(index)
        }
    }

    var oldIndexOffset = 0
    var newIndexOffset = 0

    // For simplicity, the code below uses `tableView.moveRowAtIndex` to move rows around directly.
    // You may want to move rows in your content array and then call `tableView.reloadData()` instead.
    tableView.beginUpdates()
    for oldIndex in oldIndexes {
        if oldIndex < row {
            tableView.moveRow(at: oldIndex + oldIndexOffset, to: row - 1)
            oldIndexOffset -= 1
        } else {
            tableView.moveRow(at: oldIndex, to: row + newIndexOffset)
            newIndexOffset += 1
        }
    }
    tableView.endUpdates()

    return true
}

答案 1 :(得分:14)

如果你看一下IB中的工具提示,你会看到你所指的选项

- (BOOL)allowsColumnReordering

控制,以及列重新排序。除了标准drag-and-drop API for table views之外,我不相信还有其他方法可以做到这一点。

编辑: (2012-11-25)

答案是指NSTableViewColumns的拖放重新排序;虽然这是当时公认的答案。现在差不多3年没有出现。为了使信息对搜索者有用,我将尝试给出更正确的答案。

没有允许在Interface Builder中拖放NSTableView行的重新排序的设置。您需要实现某些NSTableViewDataSource方法,包括:

- tableView:acceptDrop:row:dropOperation:

- (NSDragOperation)tableView:(NSTableView *)aTableView validateDrop:(id < NSDraggingInfo >)info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)operation

- (BOOL)tableView:(NSTableView *)aTableView writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard *)pboard

还有其他问题可以合理地解决这个问题,包括this one

答案 2 :(得分:7)

@Ethan的解决方案-更新Swift 4

viewDidLoad中:

private var dragDropType = NSPasteboard.PasteboardType(rawValue: "private.table-row")

override func viewDidLoad() {
    super.viewDidLoad()

    myTableView.delegate = self
    myTableView.dataSource = self
    myTableView.registerForDraggedTypes([dragDropType])
}

稍后扩展代表:

extension MyViewController: NSTableViewDelegate, NSTableViewDataSource {

    // numerbOfRow and viewForTableColumn methods

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

        let item = NSPasteboardItem()
        item.setString(String(row), forType: self.dragDropType)
        return item
    }

    func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation {

        if dropOperation == .above {
            return .move
        }
        return []
    }

    func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableView.DropOperation) -> Bool {

        var oldIndexes = [Int]()
        info.enumerateDraggingItems(options: [], for: tableView, classes: [NSPasteboardItem.self], searchOptions: [:]) { dragItem, _, _ in
            if let str = (dragItem.item as! NSPasteboardItem).string(forType: self.dragDropType), let index = Int(str) {
                oldIndexes.append(index)
            }
        }

        var oldIndexOffset = 0
        var newIndexOffset = 0

        // For simplicity, the code below uses `tableView.moveRowAtIndex` to move rows around directly.
        // You may want to move rows in your content array and then call `tableView.reloadData()` instead.
        tableView.beginUpdates()
        for oldIndex in oldIndexes {
            if oldIndex < row {
                tableView.moveRow(at: oldIndex + oldIndexOffset, to: row - 1)
                oldIndexOffset -= 1
            } else {
                tableView.moveRow(at: oldIndex, to: row + newIndexOffset)
                newIndexOffset += 1
            }
        }
        tableView.endUpdates()

        return true
    }

}

另外,对于可能涉及的内容:

  1. 如果要禁用某些单元格使其不可拖动,请以nil的方法返回pasteboardWriterForRow

  2. 如果要防止掉落某些位置(例如距离太远),只需在return []的方法中使用validateDrop

  3. 请勿在内部同步调用tableView.reloadData() func tableView(_ tableView:, acceptDrop info:, row:, dropOperation:)。这将干扰拖放动画,并且可能会造成混乱。找到一种方法等待动画完成,然后异步重新加载

答案 3 :(得分:5)

此答案涵盖 Swift 3 ,基于视图的NSTableView以及单行/多行拖放重新排序。

为实现这一目标,必须执行两个主要步骤:

  • 将表视图注册到可以拖动的特定允许类型的对象。

    tableView.register(forDraggedTypes: ["SomeType"])

  • 实施3个NSTableViewDataSource方法:writeRowsWithvalidateDropacceptDrop

在拖动操作开始之前,在粘贴板中存储IndexSet包含将被拖动的行的索引。

func tableView(_ tableView: NSTableView, writeRowsWith rowIndexes: IndexSet, to pboard: NSPasteboard) -> Bool {

        let data = NSKeyedArchiver.archivedData(withRootObject: rowIndexes)
        pboard.declareTypes(["SomeType"], owner: self)
        pboard.setData(data, forType: "SomeType")

        return true
    }

仅当拖动操作高于指定行时才验证drop。这确保了在执行拖动时,当拖动的行将浮动在其上方时,其他行不会突出显示。此外,这还解决了AutoLayout问题。

    func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, 
proposedDropOperation dropOperation: NSTableViewDropOperation) -> NSDragOperation {

    if dropOperation == .above {
        return .move
    } else {
        return []
    }
}

接受drop时,只需检索以前保存在粘贴板中的IndexSet, 迭代它并使用计算的索引移动行。 注意:迭代和行移动的部分我已经从@ Ethan的答案中复制了。

    func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableViewDropOperation) -> Bool {
        let pasteboard = info.draggingPasteboard()
        let pasteboardData = pasteboard.data(forType: "SomeType")

        if let pasteboardData = pasteboardData {

            if let rowIndexes = NSKeyedUnarchiver.unarchiveObject(with: pasteboardData) as? IndexSet {
                var oldIndexOffset = 0
                var newIndexOffset = 0

                for oldIndex in rowIndexes {

                    if oldIndex < row {
                        // Dont' forget to update model

                        tableView.moveRow(at: oldIndex + oldIndexOffset, to: row - 1)
                        oldIndexOffset -= 1
                    } else {
                        // Dont' forget to update model

                        tableView.moveRow(at: oldIndex, to: row + newIndexOffset)
                        newIndexOffset += 1
                    }
                }
            }
        }

        return true
    }

基于视图的NSTableView在调用moveRow时更新自己,无需使用beginUpdates()endUpdates()阻止。

答案 4 :(得分:4)

不幸的是,您必须编写粘贴板代码。 Drag and Drop API非常通用,非常灵活。但是,如果你只是需要重新排序它有点过分恕我直言。但无论如何,我创建了一个small sample project,其中有NSOutlineView,您可以在其中添加和删除项目以及重新排序。

这不是NSTableView,而是Drag&amp; amp;的执行。 Drop协议基本相同。

我一次实施了拖放操作,因此最好查看this commit

screenshot

答案 5 :(得分:3)

如果您当时只移动一行,则可以使用以下代码:

    func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableViewDropOperation) -> Bool {
        let pasteboard = info.draggingPasteboard()
        guard let pasteboardData = pasteboard.data(forType: basicTableViewDragAndDropDataType) else { return false }
        guard let rowIndexes = NSKeyedUnarchiver.unarchiveObject(with: pasteboardData) as? IndexSet else { return false }
        guard let oldIndex = rowIndexes.first else { return false }

        let newIndex = oldIndex < row ? row - 1 : row
        tableView.moveRow(at: oldIndex, to: newIndex)

        // Dont' forget to update model

        return true
    }

答案 6 :(得分:1)

Swift 5 解决方案。我必须在 viewDidLoad 中添加“registerForDraggedTypes”方法才能使其工作。

 override func viewDidLoad() {
    super.viewDidLoad()
    
    // Do any additional setup after loading the view.

    tableView.dataSource = self
    tableView.delegate = self

    // you must register the type you want to drag-n-drop! in this case 'strings'
    tableView.registerForDraggedTypes([.string])
    

    self.mapView.fitAll(in: teamManager.group.teams(), andShow: true)

}

extension ViewController : NSTableViewDataSource {
    func numberOfRows(in tableView: NSTableView) -> Int {
        return dataModel.count // or whatever
    }
    
    func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? {
        let pasteboard = NSPasteboardItem()
            
        // in this example I'm dragging the row index. Once dropped i'll look up the value that is moving by using this.
        // remember in viewdidload I registered strings so I must set strings to pasteboard
        pasteboard.setString("\(row)", forType: .string)
        return pasteboard
    }
    
    
    func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation {
        
        let canDrop = (row > 2) // in this example you cannot drop on top two rows
        print("valid drop \(row)? \(canDrop)")
        if (canDrop) {
            return .move //yes, you can drop on this row
        }
        else {
            return [] // an empty array is the equivalent of nil or 'cannot drop'
        }
    }
    
    
    func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableView.DropOperation) -> Bool {
        let pastboard = info.draggingPasteboard
        if let sourceRowString = pastboard.string(forType: .string) {
            print("from \(sourceRowString). dropping row \(row)")
            return true
        }
        
        return false
    }
}

答案 7 :(得分:0)

这是对{strong> Swift 3:

@Ethan's answer的更新
let dragDropTypeId = "public.data" // or any other UTI you want/need

func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? {
    let item = NSPasteboardItem()
    item.setString(String(row), forType: dragDropTypeId)
    return item
}

func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableViewDropOperation) -> NSDragOperation {
    if dropOperation == .above {
        return .move
    }
    return []
}

func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableViewDropOperation) -> Bool {
    var oldIndexes = [Int]()
    info.enumerateDraggingItems(options: [], for: tableView, classes: [NSPasteboardItem.self], searchOptions: [:]) {
        if let str = ($0.0.item as! NSPasteboardItem).string(forType: self.dragDropTypeId), let index = Int(str) {
            oldIndexes.append(index)
        }
    }

    var oldIndexOffset = 0
    var newIndexOffset = 0

    // For simplicity, the code below uses `tableView.moveRowAtIndex` to move rows around directly.
    // You may want to move rows in your content array and then call `tableView.reloadData()` instead.
    tableView.beginUpdates()
    for oldIndex in oldIndexes {
        if oldIndex < row {
            tableView.moveRow(at: oldIndex + oldIndexOffset, to: row - 1)
            oldIndexOffset -= 1
        } else {
            tableView.moveRow(at: oldIndex, to: row + newIndexOffset)
            newIndexOffset += 1
        }
    }
    tableView.endUpdates()

    self.reloadDataIntoArrayController()

    return true
}

答案 8 :(得分:0)

这是 swift 5 中的完整代码。让您一次移动多个项目!

override func viewDidLoad() {
        super.viewDidLoad()
        myTableView.delegate = self
        myTableView.dataSource = self
        myTableView.registerForDraggedTypes([.string])

    }       

func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? {
        let pasteboard = NSPasteboardItem()
        pasteboard.setString("\(row)", forType: .string)
        return pasteboard
    }

    func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation {
        return .move
    }

    func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableView.DropOperation) -> Bool {

        var oldIndexes = [Int]()
         info.enumerateDraggingItems(options: [], for: tableView, classes: [NSPasteboardItem.self], searchOptions: [:]) { dragItem, _, _ in
            if let str = (dragItem.item as? NSPasteboardItem)?.string(forType: .string), let index = Int(str) {
                 oldIndexes.append(index)
             }
         }

         var oldIndexOffset = 0
         var newIndexOffset = 0

         // For simplicity, the code below uses `tableView.moveRowAtIndex` to move rows around directly.
         // You may want to move rows in your content array and then call `tableView.reloadData()` instead.
         tableView.beginUpdates()
         for oldIndex in oldIndexes {
             if oldIndex < row {
                 tableView.moveRow(at: oldIndex + oldIndexOffset, to: row - 1)
                 oldIndexOffset -= 1
             } else {
                 tableView.moveRow(at: oldIndex, to: row + newIndexOffset)
                 newIndexOffset += 1
             }
         }
         tableView.endUpdates()

         return true
    }