Reputation: 33
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.
Upvotes: 0
Views: 312
Reputation: 15598
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
}
Upvotes: 2
Reputation: 33
(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
}
}
Upvotes: 0