【发布时间】:2011-01-08 11:28:54
【问题描述】:
我只是想知道是否有一种简单的方法可以设置 NSTableView 以允许它重新排序其行而无需编写任何粘贴板代码。我只需要它能够在一个表中内部执行此操作。我编写板代码没有问题,除了我相当确定我看到 Interface Builder 在某处有一个切换/看到它默认工作。这似乎是一项足够常见的任务。
谢谢
【问题讨论】:
标签: macos drag-and-drop nstableview
我只是想知道是否有一种简单的方法可以设置 NSTableView 以允许它重新排序其行而无需编写任何粘贴板代码。我只需要它能够在一个表中内部执行此操作。我编写板代码没有问题,除了我相当确定我看到 Interface Builder 在某处有一个切换/看到它默认工作。这似乎是一项足够常见的任务。
谢谢
【问题讨论】:
标签: macos drag-and-drop nstableview
将表格视图的数据源设置为符合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
}
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
}
【讨论】:
enumerateDraggingItemsWithOptions 将不再使用 nil(我使用 [.Concurrent] 代替),并且 .toInt() 不再有效(使用 Int() 代替) .
.Concurrent,因为我们想按顺序将index 附加到oldIndexes。
tableView.registerForDraggedTypes 中使用 private.table-row 而不是 public.data。
如果您查看 IB 中的工具提示,您会看到您引用的选项
- (BOOL)allowsColumnReordering
控制,好吧,列重新排序。我认为除了表格视图的标准拖放 API 之外,没有其他方法可以做到这一点。
编辑: (2012-11-25)
答案是指NSTableViewColumns的拖放重新排序;虽然这是当时公认的答案。近 3 年过去了,这似乎并不正确。为了使信息对搜索者有用,我将尝试给出更正确的答案。
没有允许在 Interface Builder 中拖放对 NSTableView 行重新排序的设置。您需要实现某些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
还有其他一些问题可以相当彻底地解决这个问题,包括this one
苹果链接到Drag and Drop APIs。
【讨论】:
@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
}
}
另外,对于那些它可能关心的人:
如果您想禁用某些单元格不可拖动,请在pasteboardWriterForRows 方法中返回nil
如果您想防止掉落某个位置(例如太远),只需在validateDrop 的方法中使用return []
不要在内部同步调用 tableView.reloadData() func tableView(_ tableView:, acceptDrop info:, row:, dropOperation:)。这会干扰拖放动画,并且可能会非常混乱。找到一种方法等到动画完成,然后异步重新加载
【讨论】:
此答案涵盖 Swift 3、基于视图的 NSTableViews 和单行/多行拖放重新排序。
为了实现这一点,必须执行两个主要步骤:
将表格视图注册到特别允许的可拖动对象类型。
tableView.register(forDraggedTypes: ["SomeType"])
实现 3 个 NSTableViewDataSource 方法:writeRowsWith、validateDrop 和 acceptDrop。
在拖动操作开始之前,将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() 块。
【讨论】:
不幸的是,您必须编写粘贴板代码。拖放 API 相当通用,因此非常灵活。但是,如果您只需要重新排序,恕我直言,这有点过分了。但无论如何,我创建了一个 small sample project,它有一个 NSOutlineView,您可以在其中添加和删除项目以及重新排序。
这不是NSTableView,但拖放协议的实现基本相同。
我一口气实现了拖放,所以最好看看this commit。
【讨论】:
如果您当时只移动一行,您可以使用以下代码:
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
}
【讨论】:
unarchiveObject 部分吗?该方法已被弃用。
这是对 Swift 3 的 @Ethan's answer 的更新:
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
}
【讨论】:
tableView.registerForDraggedTypes([dragDropTypeId]) 放在viewDidLoad 中
Swift 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
}
}
【讨论】:
这是 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
}
【讨论】: