【问题标题】:Drag & Drop Reorder Rows on NSTableView在 NSTableView 上拖放重新排序行
【发布时间】:2011-01-08 11:28:54
【问题描述】:

我只是想知道是否有一种简单的方法可以设置 NSTableView 以允许它重新排序其行而无需编写任何粘贴板代码。我只需要它能够在一个表中内部执行此操作。我编写板代码没有问题,除了我相当确定我看到 Interface Builder 在某处有一个切换/看到它默认工作。这似乎是一项足够常见的任务。

谢谢

【问题讨论】:

    标签: macos drag-and-drop nstableview


    【解决方案1】:

    将表格视图的数据源设置为符合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
    }
    

    【讨论】:

    • 优秀而有用的答案。请注意,截至撰写时(2015 年 12 月),它不再完全正确,因为 enumerateDraggingItemsWithOptions 将不再使用 nil(我使用 [.Concurrent] 代替),并且 .toInt() 不再有效(使用 Int() 代替) .
    • 您可能不想使用.Concurrent,因为我们想按顺序将index 附加到oldIndexes
    • 您对如何重组/交换模型数组有任何提示吗?
    • 在学习 Swift 3 示例时,不要忘记在 tableView.registerForDraggedTypes 中使用 private.table-row 而不是 public.data
    【解决方案2】:

    如果您查看 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

    【讨论】:

    • 为什么会影响重新排序行?
    • 这看起来像是一个误读问题的案例。虽然这个问题已经快 3 年了,但我已经尝试为未来的搜索者解决它。感谢您指出错误。
    • 这是 SO 的一大优点 ;) 感谢您添加更多信息。
    【解决方案3】:

    @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
        }
    
    }
    

    另外,对于那些它可能关心的人:

    1. 如果您想禁用某些单元格不可拖动,请在pasteboardWriterForRows 方法中返回nil

    2. 如果您想防止掉落某个位置(例如太远),只需在validateDrop 的方法中使用return []

    3. 不要在内部同步调用 tableView.reloadData() func tableView(_ tableView:, acceptDrop info:, row:, dropOperation:)。这会干扰拖放动画,并且可能会非常混乱。找到一种方法等到动画完成,然后异步重新加载

    【讨论】:

      【解决方案4】:

      此答案涵盖 Swift 3、基于视图的 NSTableViews 和单行/多行拖放重新排序。

      为了实现这一点,必须执行两个主要步骤:

      • 将表格视图注册到特别允许的可拖动对象类型。

        tableView.register(forDraggedTypes: ["SomeType"])

      • 实现 3 个 NSTableViewDataSource 方法:writeRowsWithvalidateDropacceptDrop

      在拖动操作开始之前,将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() 块。

      【讨论】:

        【解决方案5】:

        不幸的是,您必须编写粘贴板代码。拖放 API 相当通用,因此非常灵活。但是,如果您只需要重新排序,恕我直言,这有点过分了。但无论如何,我创建了一个 small sample project,它有一个 NSOutlineView,您可以在其中添加和删除项目以及重新排序。

        这不是NSTableView,但拖放协议的实现基本相同。

        我一口气实现了拖放,所以最好看看this commit

        【讨论】:

          【解决方案6】:

          如果您当时只移动一行,您可以使用以下代码:

              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
              }
          

          【讨论】:

          • 知道如何使用 Swift 5 完成 unarchiveObject 部分吗?该方法已被弃用。
          【解决方案7】:

          这是对 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
          }
          

          【讨论】:

          • 请解释您提供此更新的原因,即它改进了什么
          • @ThomasTempelmann 10 月 6 日,他的回答没有 Swift 3 版本。根据the Meta,我发布了一个新答案,而不是编辑一个已经存在的答案。
          • 也不要忘记将tableView.registerForDraggedTypes([dragDropTypeId]) 放在viewDidLoad
          • 终于允许我一次拖动多行。谢谢!
          【解决方案8】:

          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
              }
          }
          

          【讨论】:

            【解决方案9】:

            这是 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
                }
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多