Данная статья является переводом оригинальной статьи Пола Хансена How to add drag and drop to your app.

Перетаскивание — это чрезвычайно полезная функция, поэтому не удивляйтесь, если ваши пользователи отправят вам электронное письмо с просьбой добавить ее. Несмотря на то, что и UITableView, и UICollectionView поддерживают перетаскивание, для настройки все равно требуется изрядный объем работы.

Чтобы опробовать это сейчас, давайте создадим новое приложение в Xcode. Нам нужно поместить во ViewController две TableView, оба заполненные примерами данных.

Для этого нам нужно:

  • Создать две TableView и два массива строк, заполненных «Left» и «Right».

  • Настроить оба TableView так, чтобы они использовали ViewController в качестве источника данных.

  • Задать TableView жестко закодированные фреймы.

  • Зарегистрировать ячейку и затем добавьте их в представление.

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

  • Реализовать метод делегата cellForRowAt для отображения правильно элемента из одного из двух массивов строк в зависимости от того, какая это таблица.

Замените содержимое ViewController.swift этим:

import UIKit

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    var leftTableView = UITableView()
    var rightTableView = UITableView()

    var leftItems = [String](repeating: "Left", count: 20)
    var rightItems = [String](repeating: "Right", count: 20)

    override func viewDidLoad() {
        super.viewDidLoad()

        leftTableView.dataSource = self
        rightTableView.dataSource = self

        leftTableView.frame = CGRect(x: 0, y: 40, width: 150, height: 400)
        rightTableView.frame = CGRect(x: 150, y: 40, width: 150, height: 400)

        leftTableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        rightTableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")

        view.addSubview(leftTableView)
        view.addSubview(rightTableView)
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if tableView == leftTableView {
            return leftItems.count
        } else {
            return rightItems.count
        }
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)

        if tableView == leftTableView {
            cell.textLabel?.text = leftItems[indexPath.row]
        } else {
            cell.textLabel?.text = rightItems[indexPath.row]
        }

        return cell
    }
}

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

Первый шаг — указать обоим табличным представлениям использовать текущий ViewController в качестве делегата перетаскивания, а затем активировать возможность перетаскивания для них обоих. Добавьте этот код в viewDidLoad():

leftTableView.dragDelegate = self
leftTableView.dropDelegate = self
rightTableView.dragDelegate = self
rightTableView.dropDelegate = self

leftTableView.dragInteractionEnabled = true
rightTableView.dragInteractionEnabled = true

Xcode выдаст несколько предупреждений, потому что наш текущий класс контроллера представления не соответствует протоколам UITableViewDragDelegate или UITableViewDropDelegate. Это можно исправить, добавив эти два протокола в наш класс — прокрутите вверх до самого верха и измените определение класса на это:

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITableViewDragDelegate, UITableViewDropDelegate 

Это, в свою очередь, создает еще одну проблему: мы говорим, что соответствуем этим двум новым протоколам, но не реализуем их требуемые методы. Xcode может автоматически создать требуемые методы для протоколов. Нажмите «Исправить», в появившемся сообщении об ошибке чтобы Xcode вставил для нас два отсутствующих метода:

func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {

}

func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {

}

Метод itemsForBeginning проще, поэтому давайте начнем с него. Он вызывается, когда пользователь инициировал операцию перетаскивания ячейки табличного представления, удерживая палец нажатым, и должен вернуть массив элементов перетаскивания. Если вы возвращаете пустой массив, вы фактически отказываетесь от перетаскивания.

Мы собираемся предоставить этому методу четыре строки кода:

  1. Выясним, какая строка копируется. Мы можем сделать это с помощью простого тернарного оператора: если рассматриваемое табличное представление является левым табличным представлением, то считываем из массива левых элементов, в противном случае считываем из массива правых элементов.

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

  3. Поместим эти данные в NSItemProvider, пометив их как содержащие строку обычного текста, чтобы другие приложения знали, что с ними делать.

  4. Наконец, поместим этот NSItemProvider внутрь UIDragItem, чтобы его можно было использовать для перетаскивания с помощью UIKit.

Чтобы пометить данные элемента как обычный текст, нам нужно импортировать MobileCoreServices, поэтому добавим эту строку кода в верхней части ViewController.swift:

import MobileCoreServices

Реализуем метод itemsForBeginning на следующим образом:

func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
    let string = tableView == leftTableView ? leftItems[indexPath.row] : rightItems[indexPath.row]
    guard let data = string.data(using: .utf8) else { return [] }
    let itemProvider = NSItemProvider(item: data as NSData, typeIdentifier: kUTTypePlainText as String)

    return [UIDragItem(itemProvider: itemProvider)]
}

Теперь нам нужно заполнить performDropWith, что довольно сложно, потому что есть две потенциальные сложности.

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

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

Сначала, самая простая часть: выяснить, куда вставлять строки. Метод performDropWith передает нам объект класса UITableViewDropCoordinator, у которого есть свойство destinationIndexPath, который сообщает нам, куда пользователь хочет вставить данные. Однако это необязательно: значение будет равно nil, если пользователь перетащит свои данные в некое пустое место в нашем табличном представлении, и если это произойдет, мы будем предполагать, что он хотел вставить данные в конец таблицы.

Начнем с добавления этого кода в метод performDropWith:

let destinationIndexPath: IndexPath

if let indexPath = coordinator.destinationIndexPath {
    destinationIndexPath = indexPath
} else {
    let section = tableView.numberOfSections - 1
    let row = tableView.numberOfRows(inSection: section)
    destinationIndexPath = IndexPath(row: row, section: section)
}

Как вы можете видеть, мы либо используем destinationIndexPath координатора, если он существует, либо создаеем destinationIndexPath, указывающий на последнюю строку последнего раздела.

Следующий шаг — попросить drop coordinator загрузить все имеющиеся у него объекты для определенного класса, которым в нашем случае будет NSString.

Нам нужно вызвать нужный фрагмент кода, когда элементы будут готовы, и здесь начинается сложность: нам нужно вставить их все один за другим ниже destinationIndexPath, изменив массивы leftItems или rightItems, прежде чем, наконец, вызвать insertRows() в нашем табличном представлении, чтобы заставить изменения отобразиться.

Итак, мы только что написали код для определения destinationindexpath для операции вставки. Но если мы получаем несколько элементов, то все, что у нас есть, это index path для первого элемента. Второй элемент должен быть на одну строку ниже, третий элемент должен быть на две строки ниже и так далее. По мере продвижения вниз по каждому элементу для копирования мы собираемся создать новый index path и сохранить его в массиве indexPaths.

Добавим этот код в свой метод performDropWith под предыдущим кодом, который мы только что написали:

coordinator.session.loadObjects(ofClass: NSString.self) { items in
    guard let strings = items as? [String] else { return }
                                                         
    var indexPaths = [IndexPath]()

    for (index, string) in strings.enumerated() {
        let indexPath = IndexPath(row: destinationIndexPath.row + index, section: destinationIndexPath.section)

        if tableView == self.leftTableView {
            self.leftItems.insert(string, at: indexPath.row)
        } else {
            self.rightItems.insert(string, at: indexPath.row)
        }

        indexPaths.append(indexPath)
    }

    tableView.insertRows(at: indexPaths, with: .automatic)
}

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

Комментарии (1)


  1. Gargo
    13.12.2023 13:07

    зачем вы удалили комментарии из последнего куска кода (есть в оригинале)?