Когда речь идет о создании современных и привлекательных пользовательских интерфейсов, функциональность Drag and Drop (перетаскивание и сброс) играет ключевую роль. Эта техника позволяет пользователям более естественным образом взаимодействовать с контентом и упрощает перемещение элементов внутри приложения.

В данной статье мы погрузимся в мир Drag and Drop в контексте UICollectionView, одного из наиболее мощных и гибких компонентов пользовательского интерфейса в iOS. Попробуем легко и эффективно внедрить эту функциональность в проекты, создавая интерактивные и удобные интерфейсы для пользователей.

Собственно это то, что у нас получится:


В этой статье я не буду создавать ячейку и модель в отдельном файле, я думаю при желании с этим справиться каждый, настраивать все буду в основном вью контроллере

import UIKit

class ViewController: UIViewController {

  // Создаем массив загруженных картинок
    var emoji = [
        "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"
    ]
    
    // Инициализируем коллекцию
    lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.minimumInteritemSpacing = 5
        layout.minimumLineSpacing = 5
        let collection = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collection.backgroundColor = .clear
        collection.translatesAutoresizingMaskIntoConstraints = false
        return collection
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
        setupConstraints()
    }
    
    func setupViews() {

      // Создаем градиент
        let gradientLayer = CAGradientLayer()
        gradientLayer.frame = view.bounds
        
        // Определяем цвета для градиента
        let rainbowColors: [CGColor] = [
            UIColor.systemYellow.cgColor,
            UIColor.systemGreen.cgColor,
            UIColor.systemMint.cgColor,
            UIColor.systemIndigo.cgColor,
        ]
        
        gradientLayer.colors = rainbowColors
        
        // Определяем направление градиента (например, сверху вниз)
        gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
        gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
        
        // Добавляем слой с градиентом на задний фон
        view.layer.insertSublayer(gradientLayer, at: 0)
        
        view.addSubview(collectionView)
    }

  // Расставляем констрейнты для коллекции
    func setupConstraints() {
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 50),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 5),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -5),
            collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 50)
        ])
    }
    
    func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath:IndexPath, collectionView: UICollectionView) {
        
        if let item = coordinator.items.first,
           let sourceIndexPath = item.sourceIndexPath {
            
            collectionView.performBatchUpdates({
                self.emoji.remove(at: sourceIndexPath.item)
                self.emoji.insert(item.dragItem.localObject as! String, at: destinationIndexPath.item)
                
                collectionView.deleteItems(at: [sourceIndexPath])
                collectionView.insertItems(at: [destinationIndexPath])
            }, completion: nil)
            coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
        }
    }
}

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

  1. Этот метод принимает 3 параметра:

  • coordinator: Экземпляр UICollectionViewDropCoordinator, который предоставляет информацию о текущей операции перетаскивания и сброса

  • destinationIndexPath: Индекс, куда будет перемещен элемент

  • collectionView: Коллекция, в которой происходит операция

  1. Сначала метод пытается извлечь информацию о перемещаемом элементе. Он проверяет, есть ли элемент в coordinator.items, и если такой элемент есть, то извлекает его. Затем он также пытается получить исходный индекс перемещаемого элемента (sourceIndexPath) из item.sourceIndexPath

  2. Внутри блока collectionView.performBatchUpdates выполняются несколько действий в одной анимированной транзакции:

    • Удаляется элемент из исходной позиции (sourceIndexPath) в коллекции self.emoji

    • Вставляется элемент в новую позицию (destinationIndexPath) в коллекции self.emoji

    • Удаляется элемент из исходной позиции (sourceIndexPath) в коллекции collectionView

    • Вставляется элемент в новую позицию (destinationIndexPath) в коллекции collectionView

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

    1. После того как элемент был перемещен в коллекции и обновлен пользовательский интерфейс, метод вызывает coordinator.drop(item.dragItem, toItemAt: destinationIndexPath) чтобы сообщить системе, что перемещение элемента было завершено успешно, и она может выполнить необходимые завершающие действия, связанные с операцией перетаскивания и сброса.


Теперь переходим к делегатам, мы должны реализовать UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UICollectionViewDropDelegate и UICollectionViewDragDelegate

Прежде всего добавляем настройки в нашу коллекцию:

    lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.minimumInteritemSpacing = 5
        layout.minimumLineSpacing = 5
        let collection = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collection.backgroundColor = .clear
        collection.translatesAutoresizingMaskIntoConstraints = false
        collection.dragInteractionEnabled = true

      // Регистрируем ячейку и устанавливаем делегаты
        collection.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "CELL")
        collection.delegate = self
        collection.dataSource = self
        collection.dragDelegate = self
        collection.dropDelegate = self
        return collection
    }()

Переходим к реализации протокола UICollectionViewDropDelegate

extension ViewController: UICollectionViewDropDelegate {
    
    func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
        
        if collectionView.hasActiveDrag {
            return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
        }
        return UICollectionViewDropProposal(operation: .forbidden)
    }
    
    func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
        
        var destinationIndexPath: IndexPath
        if let indexPath = coordinator.destinationIndexPath {
            destinationIndexPath = indexPath
        } else {
            let row = collectionView.numberOfItems(inSection: 0)
            destinationIndexPath = IndexPath(item: row - 1, section: 0)
        }
        
        if coordinator.proposal.operation == .move {
            self.reorderItems(coordinator: coordinator, destinationIndexPath: destinationIndexPath, collectionView: collectionView)
        }
    }
}

В данном расширении используем два метода:

  1. collectionView(_:dropSessionDidUpdate:withDestinationIndexPath:)

  • Метод вызывается при каждом обновлении состояния сессии перетаскивания и сброса (dropSession). Он принимает три параметра: collectionView, session (сессия перетаскивания) и destinationIndexPath (индекс назначения, куда будет выполняться сброс элемента)

  • Сначала метод проверяет, есть ли активная сессия перетаскивания в collectionView. Это делается с помощью проверки collectionView.hasActiveDrag. Если кто-то перетаскивает элемент в данный момент, то метод возвращает UICollectionViewDropProposal с операцией .move и намерением .insertAtDestinationIndexPath. Это означает, что в этом случае предполагается перемещение элемента и он должен быть вставлен в индекс, указанный в destinationIndexPath

  • Если активного перетаскивания нет, метод возвращает UICollectionViewDropProposal с операцией .forbidden , что означает, что в этом случае сброс элемента запрещен и не будет выполнен

  1. collectionView(_collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator)

  • Метод вызывается, когда пользователь выполняет операцию перетаскивания и сброса элементов. Он принимает два параметра: collectionView, который представляет коллекцию, в которую выполняется сброс, и coordinator, который предоставляет информацию о текущей операции перетаскивания и сброса

  • Сначала метод пытается определить индекс пункта назначения (destinationIndexPath) куда будет выполнен сброс элемента. Это делается следующим образом:

    Проверяется, есть ли у coordinator уже определенный destinationIndexPath (то есть индекс, куда пользователь собирается сбросить элемент). Если такой индекс уже определен, он используется.

    Если индекс не определен (например, если пользователь сбрасывает элемент вне явно определенной области), то метод вычисляет индекс, основываясь на текущем количестве элементов в секции 0 (section 0). Он берет количество элементов и вычитает 1, чтобы получить индекс последнего элемента в секции 0.

  • Затем метод проверяет тип операции, которую пользователь пытается выполнить с перетаскиваемым элементом. Он использует coordinator.proposal.operation, чтобы определить тип операции. В данном случае, метод проверяет, является ли операция операцией перемещения (.move)

  • Если тип операции - перемещение (.move), то вызывается другой метод reorderItems(coordinator:destinationIndexPath:collectionView:) который мы определили ранее


Теперь давайте реализуем протокол UICollectionViewDragDelegate, который позволяет настроить поведение перетаскивания элементов

extension ViewController: UICollectionViewDragDelegate {
    
    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        let item = self.emoji[indexPath.row]
        let itemProvider = NSItemProvider(object: item as NSString)
        let dragItem = UIDragItem(itemProvider: itemProvider)
        dragItem.localObject = item
        return [dragItem]
    }
}

Данный метод вызывается, когда пользователь начинает перетаскивать элемент из коллекции. Он принимает три параметра:

  • collectionView: Коллекция, из которой начинается перетаскивание элемента

  • session: Объект UIDragSession, представляющий текущую сессию перетаскивания

  • indexPath: Индекс элемента, который начинает перетаскиваться

Внутри метода создается элемент (UIDragItem) , который представляет сам элемент данных, который будет перемещен.

  • Сначала элемент данных (в данном случае строка с эмодзи) извлекается из массива self.emoji по индексу indexPath.row

  • Затем создается NSItemProvider, который является объектом, предоставляющим данные элемента для системы перетаскивания

  • Создается объект UIDragItem с использованием NSItemProvider, который будет предоставлять данные элемента во время перетаскивания

  • Наконец, элемент данных (item) устанавливается как локальный объект (localObject) в dragItem. Это позволяет приложению отслеживать перемещаемый элемент и использовать его информацию во время перетаскивания

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


Ну и наконец реализуем всеми знакомые протоколы UICollectionViewDelegateFlowLayout и UICollectionViewDataSource

extension ViewController: UICollectionViewDelegateFlowLayout {

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let square = (collectionView.frame.width - 10) / 3
        return CGSize(width: square, height: square)
    }
}

extension ViewController: UICollectionViewDataSource {
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return emoji.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CELL", for: indexPath)
        cell.backgroundColor = .white
        cell.layer.cornerRadius = 5
        cell.clipsToBounds = true
        let image = UIImage(named: self.emoji[indexPath.row])
        let cellImage = UIImageView(image: image)
        cellImage.contentMode = .scaleAspectFill
        cell.contentView.addSubview(cellImage)
        cell.contentView.clipsToBounds = true
        cellImage.translatesAutoresizingMaskIntoConstraints = false
        cellImage.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 4).isActive = true
        cellImage.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: 4).isActive = true
        cellImage.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -4).isActive = true
        cellImage.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -4).isActive = true
        return cell
    }
}

Заключение

Мы рассмотрели базовый вариант использования техники Drag and Drop, с этим фундаментальным пониманием, вы можете дальше исследовать и применять более сложные сценарии ваших проектов, создавая более интересные и функциональные приложения.

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


  1. Gargo
    09.09.2023 20:12

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