Головне
Свайп — це швидкий ковзний рух пальцем по поверхні екрану, який завершується відривом. Свайп може бути коротким (гортання елемента) або довгим (відкриття шторки), але ключова характеристика — дискретність: система розпізнає його як одну подію, а не як безперервну послідовність координат.
Мобільні ОС визначають свайп за трьома параметрами: мінімальна відстань (поріг), мінімальна швидкість (velocity) та напрямок. Якщо палець рухається повільно або відстань недостатня, система інтерпретує жест як панорамування (pan) або тап. Порогові значення відрізняються між платформами: iOS використовує константу UISwipeGestureRecognizer з фіксованим порогом, Android — ViewConfiguration.getScaledTouchSlop(), який повертає ~8–16 dp залежно від щільності екрану.
Згідно з Android Developers Documentation, onFling (свайп) в GestureDetector спрацьовує при швидкості вище 100 px/s та відстані від 50px. В IT Sectr ми використовуємо свайп для навігації (галереї, карткові інтерфейси, онбординг) — цей жест інтуїтивно зрозумілий користувачам будь-якого віку.
Свайп і панорамування (pan/drag) — різні типи жестів, хоча обидва використовують рух пальця по екрану. Свайп дискретний і означає «зробити одну дію», тоді як панорамування безперервне і означає «переміщати об'єкт, поки палець на екрані».
| Параметр | Свайп (Swipe) | Панорамування (Pan/Drag) |
|---|---|---|
| Тип | Дискретний | Безперервний |
| Події | Одна (розпізнано/не розпізнано) | Багато (.began → .changed → .ended) |
| Швидкість | Висока (швидкий рух) | Будь-яка (повільний рух) |
| Приклад iOS | UISwipeGestureRecognizer | UIPanGestureRecognizer |
| Приклад Android | GestureDetector.onFling() | GestureDetector.onScroll() |
| Сценарій | Видалити лист, перегорнути сторінку | Перемістити карту, перетягнути файл |
| Відстеження координат | Ні (тільки напрямок) | Так (постійне оновлення координат) |
Вибір між свайпом і панорамуванням залежить від завдання. Для дій з елементом (видалити, архівувати, відкрити меню) використовуйте свайп. Для прямого маніпулювання (переміщення, масштабування) — панорамування. Змішування жестів на одному елементі вимагає require(toFail:) або кастомної логіки.
UISwipeGestureRecognizer — підклас UIGestureRecognizer, який розпізнає дискретний свайп в одному з чотирьох напрямків. На відміну від UIPanGestureRecognizer, він не генерує проміжні .changed-події: при розпізнаванні action викликається рівно один раз.
Властивість direction (UISwipeGestureRecognizer.Direction) задає відстежуваний напрямок — .left, .right, .up, .down. Можна об'єднувати через маску: [.left, .right] для горизонтальних свайпів. Властивість numberOfTouchesRequired визначає кількість пальців — за замовчуванням 1. UISwipeGestureRecognizer не має налаштовуваного порогу відстані — iOS використовує вбудоване значення близько 20px, достатнє для надійного розпізнавання.
Оскільки UISwipeGestureRecognizer — дискретний розпізнавач, він не конфліктує з UIPanGestureRecognizer: при повільному русі спрацює pan, при швидкому — swipe. Однак при співіснуванні з UITapGestureRecognizer на одній в'ю може знадобитися require(toFail:) для запобігання хибному розпізнаванню тапу.
В Android основний API для розпізнавання свайпу — GestureDetector з колбеком OnGestureListener, а саме метод onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY). GestureDetector аналізує початкове та кінцеве положення пальця, швидкість руху по осях та відстань, після чого викликає onFling при перевищенні порогів.
Мінімальні пороги налаштовуються через ViewConfiguration: VelocityTracker.getMinVelocity() (~100 px/s) та ViewConfiguration.getScaledTouchSlop() (~8–16 dp). Кастомний OnSwipeTouchListener — поширений патерн, який спрощує API: розробнику не потрібно вручну обчислювати напрямок та швидкість. Android Support Library не містить вбудованого «SwipeDetector», тому кастомна обгортка — стандартне рішення.
Для свайпу в списках Google рекомендує використовувати ItemTouchHelper.SimpleCallback з RecyclerView, який абстрагує детекцію жесту, анімацію та колбеки onSwiped(). В IT Sectr ми використовуємо кастомний SwipeListener для ViewPager2 та карткових інтерфейсів — це дає контроль над чутливістю свайпу без шкоди для продуктивності.
Для списків (UITableView, RecyclerView) Apple та Google надають спеціалізовані API, які замінюють ручне додавання UISwipeGestureRecognizer. Ці API підтримують кнопки дій, кастомну анімацію та жести «проведи, щоб видалити» — стандартні патерни мобільних додатків.
В iOS (11+) використовується UISwipeActionsConfiguration: метод tableView(_:leadingSwipeActionsConfigurationForRowAt:) повертає масив UIContextualAction — кнопок із заголовком, стилем (destructive, normal) та обробником. Наприклад, «Видалити» — .destructive з червоним фоном, «Архів» — .normal з сірим. Система сама анімує появу кнопок і викликає completion-блок після натискання.
В Android ItemTouchHelper.SimpleCallback прикріплюється до RecyclerView через ItemTouchHelper.attachToRecyclerView(). Параметри — прапорці напрямків (start, end, up, down) та колбек onSwiped(viewHolder, direction). Всередині onSwiped викликається adapter.notifyItemRemoved(position) для видалення елемента з анімацією. ItemTouchHelper автоматично обробляє жести, конфлікти з прокруткою (scroll) та повернення елемента, якщо свайп не завершено. За даними Google I/O 2024, ItemTouchHelper використовується в 70% Android-додатків зі свайп-діями в списках.
Додає UISwipeGestureRecognizer на view для повернення до попереднього екрану. Код показує мінімальну конфігурацію розпізнавача з напрямком .left.
import UIKit
class CardViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let swipeLeft = UISwipeGestureRecognizer(
target: self,
action: #selector(handleSwipeLeft(_:))
)
swipeLeft.direction = .left
swipeLeft.numberOfTouchesRequired = 1
view.addGestureRecognizer(swipeLeft)
}
@objc private func handleSwipeLeft(_: UISwipeGestureRecognizer) {
guard let nav = navigationController else { return }
if nav.viewControllers.count > 1 {
nav.popViewController(animated: true)
} else {
nav.dismiss(animated: true)
}
}
}
Розпізнавач прив'язується до root-view контролера. Перевірка navigationController.viewControllers.count гарантує, що pop виконується лише за наявності попереднього екрану в стеку. Для модальних контролерів використовується dismiss. Код не вимагає перевірки state — UISwipeGestureRecognizer дискретний і викликає action тільки при успішному розпізнаванні.
Реалізує просту обгортку GestureDetector, яка визначає напрямок свайпу та викликає колбеки onSwipeLeft/Right.
import android.content.Context
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
open class OnSwipeTouchListener(context: Context) : View.OnTouchListener {
private val gestureDetector = GestureDetector(context, GestureListener())
override fun onTouch(v: View, event: MotionEvent): Boolean {
return gestureDetector.onTouchEvent(event)
}
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
companion object {
private const val SWIPE_THRESHOLD = 100
private const val SWIPE_VELOCITY_THRESHOLD = 100
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
val diffX = e2.x - e1!!.x
val diffY = e2.y - e1.y
if (Math.abs(diffX) > Math.abs(diffY)) {
if (Math.abs(diffX) > SWIPE_THRESHOLD
&& Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD
) {
if (diffX > 0) onSwipeRight()
else onSwipeLeft()
return true
}
} else {
if (Math.abs(diffY) > SWIPE_THRESHOLD
&& Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD
) {
if (diffY > 0) onSwipeDown()
else onSwipeUp()
return true
}
}
return false
}
}
open fun onSwipeLeft() {}
open fun onSwipeRight() {}
open fun onSwipeUp() {}
open fun onSwipeDown() {}
}
OnSwipeTouchListener розширює SimpleOnGestureListener, реалізуючи onFling. diffX/diffY визначають напрямок, SWIPE_THRESHOLD (100px) та SWIPE_VELOCITY_THRESHOLD (100 px/s) — пороги. Використання: view.setOnTouchListener(OnSwipeTouchListener(context).apply { onSwipeLeft = { dismiss() } }).
Додає кнопку видалення при свайпі вліво по комірці таблиці. Використовує вбудований API iOS 11+ без ручного розпізнавання жестів.
func tableView(
_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(
style: .destructive,
title: "Видалити"
) { _, _, completionHandler in
items.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
completionHandler(true)
}
deleteAction.backgroundColor = .systemRed
let config = UISwipeActionsConfiguration(actions: [deleteAction])
config.performsFirstActionWithFullSwipe = true
return config
}
Метод повертає UISwipeActionsConfiguration з масивом UIContextualAction. style: .destructive автоматично задає червоний фон. performsFirstActionWithFullSwipe = true дозволяє виконати дію повним свайпом без додаткового натискання. В IT Sectr ми використовуємо цей API для всіх списків з діями — він стандартизований, підтримує VoiceOver і не вимагає кастомних GestureRecognizer.
Часті запитання
Свайп — дискретний жест із швидким рухом та відривом пальця, панорамування — безперервне перетягування з відстеженням координат. UISwipeGestureRecognizer спрацьовує один раз, UIPanGestureRecognizer генерує .began → .changed → .ended. Вибір залежить від сценарію: свайп — для дій (видалити, перегорнути), pan — для переміщення (карта, слайдер).
Логіка жесту ідентична, але API відрізняється. iOS використовує UISwipeGestureRecognizer з direction. Android використовує GestureDetector.onFling() з кастомними порогами. Результат — однаковий UX при різній реалізації. Кроссплатформенні фреймворки (Flutter, React Native) абстрагують різницю через єдиний API свайпу.
В iOS (11+) використовуйте UISwipeActionsConfiguration в UITableViewDelegate. В Android — ItemTouchHelper.SimpleCallback для RecyclerView. Ці API замінюють ручні GestureRecognizer, підтримують кнопки дій та анімацію, коректно обробляють конфлікт з прокруткою та адаптуються під платформенні HIG.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також