Когда речь идет о создании современных и привлекательных пользовательских интерфейсов, функциональность 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()
который отвечает за переупорядочивание элементов в коллекции при операции перетаскивания и сброса, обновление данных и пользовательского интерфейса, а также завершение операции для системы. Разберем по шагам:
Этот метод принимает 3 параметра:
coordinator
: ЭкземплярUICollectionViewDropCoordinator
, который предоставляет информацию о текущей операции перетаскивания и сбросаdestinationIndexPath
: Индекс, куда будет перемещен элементcollectionView
: Коллекция, в которой происходит операция
Сначала метод пытается извлечь информацию о перемещаемом элементе. Он проверяет, есть ли элемент в
coordinator.items
, и если такой элемент есть, то извлекает его. Затем он также пытается получить исходный индекс перемещаемого элемента(sourceIndexPath)
изitem.sourceIndexPath
-
Внутри блока
collectionView.performBatchUpdates
выполняются несколько действий в одной анимированной транзакции:Удаляется элемент из исходной позиции (
sourceIndexPath
) в коллекцииself.emoji
Вставляется элемент в новую позицию (
destinationIndexPath
) в коллекцииself.emoji
Удаляется элемент из исходной позиции (
sourceIndexPath
) в коллекцииcollectionView
-
Вставляется элемент в новую позицию (
destinationIndexPath
) в коллекцииcollectionView
Эти операции обновляют данные и отображение коллекции так, чтобы элемент был перемещен из одной позиции в другую. Метод
performBatchUpdates
позволяет выполнить все эти операции в одной анимации для более плавного пользовательского интерфейса
После того как элемент был перемещен в коллекции и обновлен пользовательский интерфейс, метод вызывает
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)
}
}
}
В данном расширении используем два метода:
collectionView(_:dropSessionDidUpdate:withDestinationIndexPath:)
Метод вызывается при каждом обновлении состояния сессии перетаскивания и сброса
(dropSession)
. Он принимает три параметра:collectionView
,session
(сессия перетаскивания) иdestinationIndexPath
(индекс назначения, куда будет выполняться сброс элемента)Сначала метод проверяет, есть ли активная сессия перетаскивания в collectionView. Это делается с помощью проверки
collectionView.hasActiveDrag
. Если кто-то перетаскивает элемент в данный момент, то метод возвращаетUICollectionViewDropProposal
с операцией.move
и намерением.insertAtDestinationIndexPath
. Это означает, что в этом случае предполагается перемещение элемента и он должен быть вставлен в индекс, указанный вdestinationIndexPath
Если активного перетаскивания нет, метод возвращает
UICollectionViewDropProposal
с операцией.forbidden
, что означает, что в этом случае сброс элемента запрещен и не будет выполнен
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, с этим фундаментальным пониманием, вы можете дальше исследовать и применять более сложные сценарии ваших проектов, создавая более интересные и функциональные приложения.
Gargo
в cellForItemAt ячейка не создается, а переиспользуется, поэтому у вас в ячейку будут бесконечно добавляться объекты UIImageView.
Нужно или переиспользовать существующий UIImageView, или удалять старый