Перемещение нескольких строк UITableView с помощью перетаскивания - PullRequest
0 голосов
/ 29 февраля 2020

Я видел несколько постов здесь, спрашивающих об этом, и ответ, кажется, что вы по своему усмотрению. Я думаю, что взломал это достаточно просто, поэтому я хотел бы поделиться своим подходом для комментариев и улучшений.

На высоком уровне мой подход такой:

  1. Выберите одну или несколько строк обычным способом
  2. Показать маркер перетаскивания только в первой выбранной строке, и только если все выбранные строки являются смежными.
  3. Когда начинается перетаскивание, сверните высоты всех других выбранных строк, кроме первой. Так что теперь мы просто перетаскиваем одну строку вокруг.
  4. Остановим другие выбранные строки, становящиеся строкой назначения
  5. Когда перетаскивание закончится, восстановите другие выбранные строки и расположите их в таблице

enter image description here

Другие вещи, о которых я подумал или попробовал:

  • Не скрывать другие выбранные строки. Я запутался в том, как плавно перемещать их, и не смог заставить его работать
  • Если есть несколько блоков выбранных строк, тогда я не знаю, какое поведение ожидает пользователь

Для расширения с помощью нескольких фрагментов кода:

Примечание: vList - это мой UITableView


2. Показывать маркер перетаскивания только в первой выбранной строке и только в том случае, если все выбранные строки являются смежными

В Java (так как я делаю MVP, и этот Presenter используется совместно с Android) :

if (isContiguousSelection(selectedRows)) {
    view.setDragHandleToRow(selectedRows[0]);
} else {
    view.setDragHandleToRow(null);
}

boolean isContiguousSelection (int[] selectedRows) {
    if (selectedRows.length == 0) {
        return false;
    }
    if (selectedRows.length == 1) {
        return true;
    }

    for (int i=0; i<selectedRows.length-1; i++) {
        // Check the n+1th element is one more than the nth element
        if (selectedRows[i+1] != selectedRows[i]+1) {
            return false;
        }
    }
    return true;
}

В ViewController:

func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
    return indexPath == dragHandleRowCurrent
}

Теперь нам нужно вызвать canMoveRowAt. Обычно он вызывается при перезагрузке данных, но мы не хотим перезагружать весь набор данных без необходимости. Итак:

var dragHandleRowCurrent: IndexPath? = nil

func setDragHandle(to row: IndexPath?) {
    // Save selections; reload individual rows to reset handles; restore selections
    let savedSelections = saveSelections()

    if let dragHandleRowCurrent = dragHandleRowCurrent {
        // Remove dragHandle from where it previously was
        vList.reloadRows(at: [dragHandleRowCurrent], with: .automatic)
    }
    if let row = row {
        // Add dragHandle to row
        vList.reloadRows(at: [row], with: .automatic)
    }
    dragHandleRowCurrent = row
    restoreSelections(selections: savedSelections)
}

func saveSelections() -> [IndexPath] {
    return vList.indexPathsForSelectedRows ?? []
}

func restoreSelections(selections: [IndexPath]) {
    for indexPath in selections {
        vList.selectRow(at: indexPath, animated: false, scrollPosition: .none)
    }
}

Итак, теперь у нас есть ручка перетаскивания только в первой выбранной строке. Ура.


3. Когда начинается перетаскивание, сверните все остальные выбранные строки, кроме первых

Это кажется довольно простым, но есть несколько ошибок.

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    // Collapse height if row is selected while moving
    if vList.indexPathsForSelectedRows?.contains(indexPath) == true
    && activeMoveSourceIndexPath != nil {
        // This height leaves a little bit of visible row.
        // Move to top of list seems not to work when it is zero
        // But this also good as it leaves a marker for the collapsed rows when dragging
        return 1
    } else {
        return UITableView.automaticDimension
    }
}

Обратите внимание, что высота установлена ​​на 1, а не на 0, так как я не мог заставить его правильно перетаскиваться с нуля.

Но. Мы должны вызвать высоту ForRowAt. Обычно это вызывается, когда данные перезагружаются, но мы хотим запустить его, когда начинается перетаскивание. Если мы перезагрузим данные, то жест перетаскивания будет прекращен. Я нашел ответ здесь на SO: если вы вызываете beginUpdates (), а затем endUpdates (), между которыми ничего нет, то запускается heightForRowAt. Странно, но это правда. Я не знаю, как fr agile, но в данный момент все работает нормально.

Это приводит к:

var activeMoveSourceIndexPath: IndexPath? = nil // sourceIndexPath if we are in the middle of a move

func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {

<snip snip>

    // If start of a move, hide selected rows other than the source row
    if activeMoveSourceIndexPath == nil {
        activeMoveSourceIndexPath = sourceIndexPath
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) {
            self.vList.beginUpdates() // beginUpdates then endUpdates triggers heightForRowAt without reloading data
            self.vList.endUpdates()
    }

<snip snip>
}        

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


4. Остановите другие выбранные строки, становясь строкой назначения

Это было более рискованно, чем я ожидал. Нам нужно знать направление, в котором в данный момент движется перетаскивание, чтобы мы могли пропустить другие выбранные строки в правильном направлении (вверх или вниз). Любые предложения по его оптимизации?

Вот полная версия targetIndexPathForMoveFromRowAt, включая код в 3. выше:

var activeMoveSourceIndexPath: IndexPath? = nil   // sourceIndexPath if we are in the middle of a drag
var activeMoveLastDragIndexPath: IndexPath! = nil // Last known drag position. Used to determine drag direction
var activeMoveLastDragDirectionDown = true        // Last known drag direction

func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {

    let cntRows = self.tableView(vList, numberOfRowsInSection: 0)
    let cntSelected = vList.indexPathsForSelectedRows?.count ?? 1 // Should never be nil

    // Determine drag direction
    activeMoveLastDragIndexPath = activeMoveLastDragIndexPath ?? sourceIndexPath
    if proposedDestinationIndexPath > activeMoveLastDragIndexPath {
        activeMoveLastDragDirectionDown = true
    } else if proposedDestinationIndexPath < activeMoveLastDragIndexPath {
        activeMoveLastDragDirectionDown = false
    } else {
     // Leave unchanged if equal
    }
    activeMoveLastDragIndexPath = proposedDestinationIndexPath


    // When moving multiple rows, skip over those rows as destinations
    var destIndexPath: IndexPath!
    if  proposedDestinationIndexPath > sourceIndexPath
    &&  proposedDestinationIndexPath < IndexPath(row: sourceIndexPath.row + cntSelected) {

        // Proposed destination is one of the selected rows
        if  activeMoveLastDragDirectionDown {
            // Moving down to one of the selected rows
            if sourceIndexPath.row + cntSelected == cntRows {
                // Selected rows are at the end of the list, can't move after there
                destIndexPath = sourceIndexPath
            } else {
                destIndexPath = IndexPath(row: sourceIndexPath.row + cntSelected)
            }
        } else {
            // Moving up to one of the selected rows
            destIndexPath = sourceIndexPath
        }

    } else {
        // Proposed destination is not one of the selected rows
        destIndexPath = proposedDestinationIndexPath
    }

    // If start of a move, collape the selected rows other than the source row
    if activeMoveSourceIndexPath == nil {
        self.activeMoveSourceIndexPath = sourceIndexPath
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) {
            self.vList.beginUpdates() // beginUpdates then endUpdates triggers heightForRowAt without reloading data
            self.vList.endUpdates()
        }
    }

    return destIndexPath
}

5. Когда перетаскивание закончится, восстановите остальные выбранные строки и расположите их в таблице

func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
    activeMoveSourceIndexPath = nil
    activeMoveLastDragIndexPath = nil
    if sourceIndexPath == destinationIndexPath {
        // Not moving anywhere. Restore the selected rows and exit
        vList.beginUpdates() // beginUpdates-endUpdates triggers heightForRowAt without reloading data
        vList.endUpdates()
        return
    }

    // Rearrange the other selected rows in the table
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) {
        self.vList.beginUpdates()
        let selectedIndexPaths = self.vList.indexPathsForSelectedRows!
        if sourceIndexPath > destinationIndexPath {
            // Moving up, move each row into position from first to last
            var i = 0
            for selectedIndexPath in selectedIndexPaths.sorted(by: { $0.row < $1.row }) {   // Ascending
                 if selectedIndexPath == destinationIndexPath {
                     // Source row will have already been moved by framework
                     continue
                 }
                 self.vList.moveRow(at: IndexPath(row: selectedIndexPath.row), to: IndexPath(row: destinationIndexPath.row + 1 + i))
                 i = i + 1
             }
        } else {
            // Moving down, move each row into position from last to first
            var i = 0
            for selectedIndexPath in selectedIndexPaths.sorted(by: { $0.row > $1.row }) {   // Descending
                if selectedIndexPath == destinationIndexPath {
                   // Source row will have already been moved by framework
                   continue
                }
                self.vList.moveRow(at: IndexPath(row: selectedIndexPath.row), to: IndexPath(row: destinationIndexPath.row - i))
                i = i + 1
            }
        }
        self.vList.endUpdates()

        // TODO Update the data source 
    }
}

Итак, мы go. Кажется, работает нормально (с одной ошибкой, см. Ниже). Но я уверен, что вы умные люди можете улучшить это. Давайте ваши мысли!

PS Ошибка. По какой-то причине, если непрерывный блок выбранных строк включает в себя строки со второй по последнюю, последняя (невыбранная) строка представляется скрытой. Все работает хорошо, хотя. Я еще не выследил это.

...