I had a LOT of grief trying to find a way to save the result of a user's drag and drop reordering on my NSTableView into Core Data. I found a few useful bits and pieces online (like this) but because of my binding setup – my tableview's sortDescriptors are bound to my ArrayController in XCode Storyboard – I found that none of the methods were working for me. In the hope that this may help someone else who has endured the same frustration, I'm posting my solution here.
2 Answers
Only the rows between the first and last dragged rows and the drop row need reindexing. NSArrayController.rearrangeObjects()
sorts the data objects into the new order.
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 {
if let items = billablesArrayController?.arrangedObjects as? [BillableItem] {
NSAnimationContext.runAnimationGroup({(NSAnimationContext) -> Void in
// put the dragged row indexes in an IndexSet so we can calculate which rows need moving and reindexing
let rowArray = info.draggingPasteboard.pasteboardItems!.map{ Int($0.string(forType: .string)!)! }
let draggedIndexes = IndexSet(rowArray)
tableView.beginUpdates()
// rows above drop row
if draggedIndexes.first! < row {
let indexesAboveDropRow = IndexSet(draggedIndexes.first! ..< row)
// move the dragged rows down, start at the bottom to prevent the animated rows from tumbling over each other
var newIndex = row - 1
indexesAboveDropRow.intersection(draggedIndexes).reversed().forEach { oldIndex in
tableView.moveRow(at: oldIndex, to: newIndex)
items[oldIndex].sortOrder = Int16(newIndex)
newIndex -= 1
}
// reindex other rows
indexesAboveDropRow.subtracting(draggedIndexes).reversed().forEach { oldIndex in
items[oldIndex].sortOrder = Int16(newIndex)
newIndex -= 1
}
}
// rows below drop row
if row < draggedIndexes.last! {
let indexesBelowDropRow = IndexSet(row ... draggedIndexes.last!)
// move the dragged rows up
var newIndex = row
indexesBelowDropRow.intersection(draggedIndexes).forEach { oldIndex in
tableView.moveRow(at: oldIndex, to: newIndex)
items[oldIndex].sortOrder = Int16(newIndex)
newIndex += 1
}
// reindex other rows
indexesBelowDropRow.subtracting(draggedIndexes).forEach { oldIndex in
items[oldIndex].sortOrder = Int16(newIndex)
newIndex += 1
}
}
tableView.endUpdates()
}) {
// rearrange the objects in the array controller so the objects match the moved rows
// wait until the animation is finished to prevent weird or no animations
self.billablesArrayController.rearrangeObjects()
}
// save
}
return true
}

- 14,578
- 4
- 19
- 47
(NOTE: This method would probably not be appropriate for a tableView with a huge row count as we're looping through all objects and setting a new sortOrder)
To summarise the issue - it's relatively easy to get tableview reordering working, thanks to helpful SO posts like this - the difficulty is in saving this info to Core Data because the user/UI reordering of your table is overridden by the sortDescriptors on your bound ArrayController. The bound ArrayController essentially undoes the user's table row reordering. Here's my working code:
My arrayController sortDescriptors:
billablesArrayController.sortDescriptors = [NSSortDescriptor(key: "sortOrder", ascending: true)]
in my onViewDidLoad of the ViewController:
override func viewDidLoad() {
super.viewDidLoad()
// Set ViewController as dataSource for tableView and register an array of accepted drag types
billablesTableView.dataSource = self
billablesTableView.registerForDraggedTypes([.string])
}
Implement Drag and Drop methods in your ViewController:
extension JobsViewController: NSTableViewDataSource {
func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? {
let item = NSPasteboardItem()
item.setString(String(row), forType: .string)
return item
}
func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation {
if dropOperation == .above {
// billablesArrayController.sortDescriptors are bound to tableView in xcode UI
// so we remove arrayController sortDescriptors temporarily so as not to mess with user/UI table reordering
billablesArrayController.sortDescriptors = []
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: .string), let index = Int(str) {
oldIndexes.append(index)
}
}
var oldIndexOffset = 0
var newIndexOffset = 0
var selectionIndex = 0
//Start tableView reordering
tableView.beginUpdates()
for oldIndex in oldIndexes {
if oldIndex < row {
tableView.moveRow(at: oldIndex + oldIndexOffset, to: row - 1)
oldIndexOffset -= 1
selectionIndex = row - 1
} else {
tableView.moveRow(at: oldIndex, to: row + newIndexOffset)
newIndexOffset += 1
selectionIndex = row
}
}
tableView.endUpdates()
//Get items.count from ArrayController for loop
if let items = billablesArrayController?.arrangedObjects as? [BillableItem] {
var newArray = [BillableItem]()
// get the new item order from the tableView
for i in 0..<items.count {
if let view = billablesTableView.view(atColumn: 0, row: i, makeIfNecessary: false) as? NSTableCellView {
if let tableItem = view.objectValue as? BillableItem {
newArray.append(tableItem)
}
}
}
// assign new sortOrder to each managedObject based on its index position in newArray
var index = 0
for bi in newArray {
bi.sortOrder = Int16(index)
index += 1
}
}
// reinstate arrayController sortDescriptors
billablesArrayController.sortDescriptors = [NSSortDescriptor(key: "sortOrder", ascending: true)]
// assign the dragged row as the selected item
billablesArrayController.setSelectionIndex(selectionIndex)
//save 'em
if managedObjectContext.hasChanges {
do {
try self.managedObjectContext.save()
} catch {
NSSound.beep()
_ = alertDialog(question: "Error: Can't save billable items sort order.", text: error.localizedDescription, showCancel: false)
}
}
return true
}
}

- 23
- 3