我只是想知道是否有一种简单的方法可以设置 NSTableView 以允许它重新排序其行而无需编写任何粘贴板代码。我只需要它能够在一个表中内部执行此操作。我编写板代码没有问题,除了我相当确定我看到 Interface Builder 在某处有一个切换/看到它默认工作。这似乎是一项足够普遍的任务。
谢谢
我只是想知道是否有一种简单的方法可以设置 NSTableView 以允许它重新排序其行而无需编写任何粘贴板代码。我只需要它能够在一个表中内部执行此操作。我编写板代码没有问题,除了我相当确定我看到 Interface Builder 在某处有一个切换/看到它默认工作。这似乎是一项足够普遍的任务。
谢谢
将表视图的数据源设置为符合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
}
如果您查看 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。
@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
}
}
另外,对于那些可能关注的人:
如果要禁用某些单元格可拖动,请nil
在pasteboardWriterForRow
s 方法中返回
如果你想防止丢弃某个位置(例如太远),只需使用return []
invalidateDrop
的方法
不要在内部同步调用 tableView.reloadData() func tableView(_ tableView:, acceptDrop info:, row:, dropOperation:)
。这会干扰拖放动画,并且可能会非常混乱。找到一种方法等到动画完成,然后异步重新加载
此答案涵盖Swift 3、基于 View 的NSTableView
s 和单/多行拖放重新排序。
为了实现这一目标,必须执行两个主要步骤:
将表格视图注册到可拖动的特定允许类型的对象。
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
}
基于视图的NSTableView
s在moveRow
被调用时会自行更新,不需要使用beginUpdates()
和endUpdates()
阻塞。
如果您当时只移动一行,则可以使用以下代码:
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
}
这是@Ethan对Swift 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
}
斯威夫特 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
}