Перетаскивание и перетаскивание строк в NSTableView

Мне просто интересно, был ли простой способ установить NSTableView, чтобы он мог изменять порядок своих строк без написания кода. Мне нужно только это, чтобы сделать это внутри, внутри одного стола. У меня нет проблем с написанием кода pboard, за исключением того, что я уверен, что видел, что интерфейс Builder имеет переключатель для этого где-то/видел, что он работает по умолчанию. Это, безусловно, похоже на достаточно общую задачу.

Спасибо

Ответ 1

Если вы посмотрите на подсказку в IB, вы увидите, что опция, к которой вы обращаетесь,

- (BOOL)allowsColumnReordering

контролирует, ну, перестановка столбцов. Я не верю, что существует какой-либо другой способ сделать это, кроме стандартного API-интерфейса перетаскивания для табличных представлений.

ОБНОВЛЕНИЕ: (2012-11-25)

Ответ относится к переупорядочению с помощью перетаскивания NSTableViewColumns; и в то время это был принятый ответ в то время. Похоже, сейчас, спустя почти 3 года, это не так. Чтобы сделать информацию полезной для искателей, я постараюсь дать более правильный ответ.

В Интерфейсном Разработчике отсутствует настройка, позволяющая перетаскивать и переупорядочивать строки 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

Есть и другой вопрос, который достаточно разумно решает этот вопрос, включая этот

.Ссылка Apple на API перетаскивания.

Ответ 2

Задайте свой источник данных таблицы как класс, соответствующий 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
}

Ответ 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. Если вы хотите отключить перетаскивание определенных ячеек, верните nil в методе pasteboardWriterForRow

  2. Если вы хотите предотвратить падение определенных мест (например, слишком далеко), просто используйте return [] в методе validateDrop

  3. Не вызывайте tableView.reloadData() синхронно внутри func tableView(_ tableView:, acceptDrop info:, row:, dropOperation:). Это нарушит анимацию перетаскивания и может привести к путанице. Найдите способ дождаться окончания анимации и выполните асинхронную перезагрузку

Ответ 4

Этот ответ описывает Swift 3, основанную на просмотре NSTableView и перетаскивание одиночных/нескольких строк и переадресацию.

Для достижения этого необходимо выполнить 2 основных шага:

  • Регистрировать представление таблицы на специально разрешенный тип объекта, который можно перетаскивать.

    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 обновления, когда вызывается moveRow, нет необходимости использовать блок beginUpdates() и endUpdates().

Ответ 5

К сожалению, вы должны написать код для вставки. API Drag и Drop довольно универсален, что делает его очень гибким. Тем не менее, если вам просто нужно изменить порядок немного IMHO. Но в любом случае я создал небольшой пример проекта, в котором есть NSOutlineView, где вы можете добавлять и удалять элементы, а также изменять их порядок.

Это не NSTableView, а реализация Drag & Протокол отбрасывания в основном идентичен.

Я реализовал перетаскивание за один раз, поэтому лучше посмотреть этот коммит.

screenshot

Ответ 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
    }

Ответ 7

Это обновление @Ethan answer для 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
}