21

我只是想知道是否有一种简单的方法可以设置 NSTableView 以允许它重新排序其行而无需编写任何粘贴板代码。我只需要它能够在一个表中内部执行此操作。我编写板代码没有问题,除了我相当确定我看到 Interface Builder 在某处有一个切换/看到它默认工作。这似乎是一项足够普遍的任务。

谢谢

4

9 回答 9

32

将表视图的数据源设置为符合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
}

斯威夫特 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
}
于 2014-11-11T00:27:00.273 回答
16

如果您查看 IB 中的工具提示,您会看到您所指的选项

- (BOOL)allowsColumnReordering

控制,好吧,列重新排序。除了用于表格视图的标准拖放 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

还有其他一些问题可以相当彻底地解决这个问题,包括这个

Apple 链接到拖放 API

于 2010-01-25T21:59:12.777 回答
16

@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. 如果要禁用某些单元格可拖动,请nilpasteboardWriterForRows 方法中返回

  2. 如果你想防止丢弃某个位置(例如太远),只需使用return []invalidateDrop的方法

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

于 2018-09-17T13:06:18.210 回答
6

此答案涵盖Swift 3、基于 View 的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
    }

仅当拖动操作高于指定行时才验证放置。这确保了在执行拖动时,当拖动的行将浮动在它们上方时,其他行不会突出显示。此外,这解决了一个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
    }

基于视图的NSTableViews在moveRow被调用时会自行更新,不需要使用beginUpdates()endUpdates()阻塞。

于 2016-12-28T12:12:16.997 回答
4

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

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

我一口气实现了拖放,所以最好看看这个 commit

截屏

于 2013-10-24T08:03:49.980 回答
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
    }
于 2017-01-06T15:59:59.420 回答
1

这是@EthanSwift 3 的回答的更新:

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
}
于 2016-10-06T19:56:45.097 回答
1

斯威夫特 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
    }
}
于 2021-01-26T22:51:36.633 回答
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
    }
于 2021-04-14T13:40:41.887 回答