I was just wondering if there was an easy way to set an NSTableView to allow it to reorder its rows without writing any pasteboard code. I only need it to be able to do thi
@Ethan's solution - Update Swift 4
in viewDidLoad
:
private var dragDropType = NSPasteboard.PasteboardType(rawValue: "private.table-row")
override func viewDidLoad() {
super.viewDidLoad()
myTableView.delegate = self
myTableView.dataSource = self
myTableView.registerForDraggedTypes([dragDropType])
}
Later on delegate extension :
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
}
}
Plus, for those it may concerne:
If you want to disable certain cells from being dragable, return nil
in pasteboardWriterForRow
s method
If you want to prevent drop a certain locations ( too far for instance ) just use return []
in validateDrop
's method
Do not call tableView.reloadData() synchronously inside func tableView(_ tableView:, acceptDrop info:, row:, dropOperation:)
. This will disturb Drag and Drop animation, and can be very confusing. Find a way to wait until animation finishes, and async it's reloading