在NSTableView上拖放重新排序行

Jar*_*tar 16 macos drag-and-drop nstableview

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

谢谢

Eth*_*han 28

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

将这个在适当的地方(-applicationWillFinishLaunching,-awakeFromNib,-viewDidLoad或类似的东西):

tableView.registerForDraggedTypes(["public.data"])
Run Code Online (Sandbox Code Playgroud)

然后实现这三种NSTableViewDataSource方法:

tableView:pasteboardWriterForRow:
tableView:validateDrop:proposedRow:proposedDropOperation:
tableView:acceptDrop:row:dropOperation:
Run Code Online (Sandbox Code Playgroud)

这是完全工作的代码,支持拖放重新排序多行:

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
}
Run Code Online (Sandbox Code Playgroud)

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
}
Run Code Online (Sandbox Code Playgroud)


Flu*_*imp 14

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

- (BOOL)allowsColumnReordering
Run Code Online (Sandbox Code Playgroud)

控制,以及列重新排序.除了用于表视图的标准拖放API之外,我不相信还有其他任何方法可以做到这一点.

编辑: (2012-11-25)

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

没有允许NSTableView在Interface Builder中拖放行重新排序的设置.您需要实现某些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
Run Code Online (Sandbox Code Playgroud)

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

  • 看来是一个误读问题的情况。尽管这个问题已经存在了3年之久,但我已经尝试为以后的搜索者解决它。感谢您指出错误。 (2认同)

Oly*_*tre 10

@ 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])
}
Run Code Online (Sandbox Code Playgroud)

后来代表延期:

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
    }

}
Run Code Online (Sandbox Code Playgroud)

另外,对于那些可能关注的人:

  1. 如果你想禁用被dragable某些细胞,恢复nilpasteboardWriterForRowS分析

  2. 如果你想防止掉落某个位置(例如太远),只需使用return []in validateDrop方法

  3. 不要在里面同步调用tableView.reloadData() func tableView(_ tableView:, acceptDrop info:, row:, dropOperation:).这将扰乱拖放动画,并且可能非常混乱.找到一种等待动画结束的方法,并异步重新加载


Bes*_*esi 5

不幸的是,您必须编写粘贴板代码。拖放 API 相当通用,这使得它非常灵活。但是,如果您只需要重新排序,恕我直言,这有点过分了。但无论如何,我创建了一个小型示例项目NSOutlineView您可以在其中添加和删除项目以及重新排序它们。

这不是一个NSTableView,但拖放协议的实现基本上是相同的。

我一次性实现了拖放,因此最好查看此提交

截屏


tes*_*sla 5

这个答案涵盖了Swift 3,基于视图的NSTableViews和单/多行拖放重新排序.

必须执行以下两个主要步骤才能实现此目的:

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

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

  • 实施3种NSTableViewDataSource方法:writeRowsWith,validateDropacceptDrop.

在拖动操作开始之前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
    }
Run Code Online (Sandbox Code Playgroud)

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

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

    if dropOperation == .above {
        return .move
    } else {
        return []
    }
}
Run Code Online (Sandbox Code Playgroud)

当接受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
    }
Run Code Online (Sandbox Code Playgroud)

基于视图的NSTableViews在moveRow调用时更新自己,不需要使用beginUpdates()endUpdates()阻止.