Реализация Swipe Actions для элементов списка мобильного приложения
Swipe-to-delete в iOS Mail появился в 2007 году и до сих пор остаётся одним из самых копируемых паттернов. Звучит просто — пока не начинаешь делать это правильно на обеих платформах одновременно.
Нативная реализация vs кастомная
На iOS UITableView имеет встроенный механизм через UISwipeActionsConfiguration. Метод tableView(_:trailingSwipeActionsConfigurationForRowAt:) возвращает конфигурацию с массивом UIContextualAction. Каждый action имеет стиль .normal или .destructive (деструктивный автоматически окрашивается красным и показывает подтверждение). leadingSwipeActionsConfigurationForRowAt — для свайпа вправо.
Для UICollectionView свайп-действий из коробки нет — нужно либо использовать UICollectionViewListConfiguration (доступна с iOS 14), либо реализовывать через UILongPressGestureRecognizer + UIPanGestureRecognizer с кастомной логикой threshold и velocity. UICollectionViewListConfiguration — предпочтительный путь для новых приложений.
В SwiftUI — swipeActions(edge:allowsFullSwipe:content:) модификатор на элементе List. allowsFullSwipe: true разрешает полный свайп для первого действия. Ограничение: до iOS 15 модификатор недоступен, при таргете ниже нужен fallback через UIViewRepresentable.
На Android нативного компонента для свайп-действий в RecyclerView нет. Стандартный подход — ItemTouchHelper с реализацией ItemTouchHelper.SimpleCallback. Переопределяем onChildDraw() для отрисовки фона и иконок при свайпе, onSwiped() для обработки события. Важный нюанс: onChildDraw вызывается на каждый кадр при движении — вся отрисовка должна быть максимально дешёвой, Canvas операции без аллокаций.
С Jetpack Compose — библиотека compose-foundation содержит SwipeToDismiss из material3. Для полноценных действий слева и справа — rememberSwipeToDismissBoxState() и SwipeToDismissBox. Анимация строится на Animatable + LaunchedEffect.
Где разработчики теряют время
Главная проблема — состояние ячейки после свайпа при reuse. В UITableView при dequeueReusableCell ячейка возвращается в исходное положение автоматически. Но если вы делаете кастомный свайп через gesture recognizer — transform и alpha нужно сбрасывать в prepareForReuse(). Забыли — получаете ghost-артефакты в ячейках после скролла.
На Android с ItemTouchHelper аналогичная история: после notifyItemRemoved() индексы съезжают, и если не пересчитать позицию в коллбеке — удаляется не тот элемент. Плюс анимация отскока при отмене свайпа выглядит деревянно без кастомизации getAnimationDuration().
Ещё один сценарий — конфликт с горизонтальным scroll-контейнером внутри ячейки. UIScrollView и UITableView имеют конкурирующие gesture recognizer'ы. Решается через gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) с логикой приоритета по velocity direction на старте жеста.
Что входит в работу
Реализуем свайп-действия для UITableView / UICollectionView / SwiftUI List на iOS и RecyclerView / Compose на Android. Для Flutter — flutter_slidable с поддержкой ActionPane с обеих сторон. Для React Native — react-native-gesture-handler с Swipeable компонентом на основе Reanimated 2.
Покрываем edge cases: конфликт жестов, состояние при reuse, haptic при destructive action, accessibility (VoiceOver/TalkBack должны озвучивать доступные действия через accessibilityCustomActions).
Срок: 1 день — стандартные свайп-действия на одной платформе. 2–3 дня — кросс-платформенная реализация с кастомными анимациями и полным покрытием edge cases.







