Реализация Drag-and-Drop для мобильного приложения
Перетаскивание элементов выглядит как простая анимация, но за ней скрывается сложная координация: определение начала жеста, визуальная копия перетаскиваемого элемента, hit-testing зон сброса, обновление модели данных, анимация возврата при отмене. Каждый из этих шагов — потенциальный баг.
iOS: UIKit drag-and-drop
С iOS 11 Apple предоставила встроенный Drag & Drop API через UIDragInteraction и UIDropInteraction. UIDragInteraction добавляется на source view, UIDropInteraction — на target view. Данные передаются через NSItemProvider — универсальный контейнер для любых типов данных.
let dragInteraction = UIDragInteraction(delegate: self)
view.addInteraction(dragInteraction)
Ключевой метод делегата: dragInteraction(_:itemsForBeginning:) должен вернуть [UIDragItem] с NSItemProvider. Для внутреннего drag & drop (в пределах одного приложения) можно передавать любой объект через localObject свойство UIDragItem — он не сериализуется и доступен мгновенно.
Для UICollectionView и UITableView есть специализированный UICollectionViewDragDelegate / UITableViewDragDelegate с методами collectionView(_:itemsForBeginningDragSession:at:). Встроенная поддержка reorder через collectionView(_:moveItemAt:to:) — не нужно вручную управлять анимацией ячеек.
Проблемы с UICollectionView drag. При перетаскивании коллекция автоматически создаёт snapshot перетаскиваемой ячейки. Если ячейка содержит AVPlayerLayer или кастомные CALayer анимации — snapshot выглядит замороженным. Решение: переопределить dragPreviewParametersForItemAt: и вернуть UIDragPreviewParameters с кастомным visiblePath.
В SwiftUI — .draggable() и .dropDestination() модификаторы с iOS 16. Данные должны соответствовать Transferable протоколу. Кастомные типы — реализуй Transferable через CodableRepresentation или DataRepresentation.
Android: drag & drop
На Android View.startDragAndDrop() (API 24+, ранее startDrag()). DragShadowBuilder создаёт визуальную тень при перетаскивании — переопределяем onDrawShadow() для кастомного вида. View.setOnDragListener() на target view обрабатывает события ACTION_DRAG_ENTERED, ACTION_DRAG_EXITED, ACTION_DROP, ACTION_DRAG_ENDED.
В Jetpack Compose — Modifier.dragAndDropSource {} и Modifier.dragAndDropTarget {} появились в Compose 1.5. DragAndDropTransferData с ClipData как контейнер данных. Для drag внутри LazyColumn с reorder — reorderable библиотека (Calvin Liang) или compose-reorderable.
Flutter: кастомная реализация
Flutter не имеет встроенного drag & drop для мобильных платформ такого уровня как нативный. Draggable<T> и DragTarget<T> — основные виджеты. Draggable создаёт childWhenDragging (placeholder на месте оригинала) и feedback (виджет, следующий за пальцем). DragTarget.onWillAcceptWithDetails() для проверки, onAcceptWithDetails() для обработки.
Для reorder списков — ReorderableListView из Material библиотеки. onReorder коллбек получает oldIndex и newIndex. Важный баг, с которым сталкиваются: при newIndex > oldIndex нужно сделать newIndex -= 1 перед list.insert(). Документация это упоминает, но разработчики часто пропускают.
Общие проблемы реализации
Haptic при начале drag. На iOS UIFeedbackGenerator.impactOccurred() при longPressGestureRecognizer.state == .began. Пользователь чувствует «захват» объекта. Без этого drag ощущается невесомым.
Scroll во время drag. Пользователь тащит элемент к краю списка — список должен прокручиваться. В UIKit — UIScrollView автоскролл при попадании в зону 50pt от края. В Flutter — DragTarget в связке со ScrollController.animateTo() при DragTarget.onWillAccept() когда курсор в пределах 80px от края.
Отмена drag. Если пользователь бросил элемент не в допустимую зону — анимация возврата на исходную позицию. В UIKit это происходит автоматически (snap-back animation). В Flutter — управляем через Draggable.onDraggableCanceled() с Velocity и координатами.
Состояние модели данных. При реorder — обновляй dataSource синхронно с анимацией, не после. Иначе при быстром drag нескольких элементов подряд индексы разъедутся.
Срок: реализация drag & drop для конкретного экрана (список с reorder или drag между двумя зонами) — 2–3 дня. Сложная система с несколькими типами перетаскиваемых объектов, cross-view drop и кастомными анимациями — 5–7 дней.







