핵심 요점
스와이프는 화면 표면에서 손가락을 빠르게 미끄러뜨리는 동작으로, 손가락을 떼면서 종료됩니다. 스와이프는 짧게(요소 넘기기) 또는 길게(커튼 열기) 할 수 있지만, 주요 특징은 이산성입니다. 시스템이 이를 연속적인 좌표 시퀀스가 아닌 단일 이벤트로 인식합니다.
모바일 OS는 세 가지 매개변수(최소 거리(임계값), 최소 속도(velocity), 방향)를 기준으로 스와이프를 감지합니다. 손가락이 느리게 움직이거나 거리가 충분하지 않으면 시스템은 제스처를 패닝 또는 탭으로 해석합니다. 임계값은 플랫폼마다 다릅니다. iOS는 UISwipeGestureRecognizer에 고정 임계값의 상수를 사용하고, Android는 ViewConfiguration.getScaledTouchSlop()을 사용하여 화면 밀도에 따라 약 8~16dp를 반환합니다.
Android Developers Documentation에 따르면 GestureDetector의 onFling(스와이프)은 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 이벤트를 생성하지 않습니다. 인식되면 액션이 정확히 한 번 호출됩니다.
direction 속성(UISwipeGestureRecognizer.Direction)은 추적할 방향(.left, .right, .up, .down)을 설정합니다. 마스크를 사용하여 방향을 결합할 수 있습니다(수평 스와이프의 경우 [.left, .right]). numberOfTouchesRequired 속성은 손가락 수를 지정합니다(기본값 1). UISwipeGestureRecognizer에는 구성 가능한 거리 임계값이 없습니다. iOS는 약 20px의 내장 값을 사용하여 안정적인 인식을 보장합니다.
UISwipeGestureRecognizer는 이산적 인식기이므로 UIPanGestureRecognizer와 충돌하지 않습니다. 느린 움직임은 pan을, 빠른 움직임은 swipe를 트리거합니다. 그러나 같은 뷰에 UITapGestureRecognizer가 공존하는 경우 잘못된 탭 인식을 방지하기 위해 require(toFail:)이 필요할 수 있습니다.
Android에서 스와이프 인식을 위한 기본 API는 OnGestureListener 콜백이 있는 GestureDetector, 특히 onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) 메서드입니다. GestureDetector는 손가락의 초기 및 최종 위치, 축을 따른 이동 속도 및 거리를 분석하고 임계값을 초과하면 onFling을 호출합니다.
최소 임계값은 ViewConfiguration을 통해 구성할 수 있습니다: VelocityTracker.getMinVelocity()(~100 px/s) 및 ViewConfiguration.getScaledTouchSlop()(~8~16dp). 사용자 정의 OnSwipeTouchListener는 API를 간소화하는 일반적인 패턴으로, 개발자가 방향과 속도를 수동으로 계산할 필요가 없습니다. Android Support Library에는 내장 "SwipeDetector"가 포함되어 있지 않으므로 사용자 정의 래퍼가 표준 솔루션입니다.
리스트에서 스와이프의 경우 Google은 RecyclerView의 ItemTouchHelper.SimpleCallback을 권장합니다. 이는 제스처 감지, 애니메이션 및 onSwiped() 콜백을 추상화합니다. IT Sectr에서는 ViewPager2 및 카드 인터페이스에 사용자 정의 SwipeListener를 사용합니다. 이를 통해 성능 저하 없이 스와이프 감도를 제어할 수 있습니다.
리스트(UITableView, RecyclerView)를 위해 Apple과 Google은 특화된 API를 제공합니다. 이는 수동으로 UISwipeGestureRecognizer를 추가하는 것을 대체합니다. 이러한 API는 액션 버튼, 사용자 정의 애니메이션 및 "스와이프하여 삭제" 제스처를 지원하며, 이는 모바일 애플리케이션의 표준 패턴입니다.
iOS(11+)에서는 UISwipeActionsConfiguration을 사용합니다. tableView(_:leadingSwipeActionsConfigurationForRowAt:) 메서드는 UIContextualAction(제목, 스타일(destructive, normal) 및 핸들러가 있는 버튼)의 배열을 반환합니다. 예를 들어 "삭제"는 빨간색 배경의 .destructive, "보관"은 회색 배경의 .normal입니다. 시스템은 버튼의 나타남을 애니메이션화하고 탭 후 completion 블록을 호출합니다.
Android에서는 ItemTouchHelper.SimpleCallback을 ItemTouchHelper.attachToRecyclerView()를 통해 RecyclerView에 연결합니다. 매개변수에는 방향 플래그(start, end, up, down)와 onSwiped(viewHolder, direction) 콜백이 포함됩니다. onSwiped 내에서 애니메이션과 함께 항목을 제거하기 위해 adapter.notifyItemRemoved(position)이 호출됩니다. ItemTouchHelper는 제스처, 스크롤 충돌 및 스와이프가 완료되지 않은 경우 항목 복귀를 자동으로 처리합니다. Google I/O 2024에 따르면 리스트에 스와이프 액션이 있는 Android 앱의 70%에서 ItemTouchHelper가 사용됩니다.
이전 화면으로 돌아가기 위해 뷰에 UISwipeGestureRecognizer를 추가합니다. 코드는 .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)
}
}
}
인식기는 컨트롤러의 루트 뷰에 연결됩니다. navigationController.viewControllers.count 검사는 스택에 이전 화면이 있을 때만 pop이 수행되도록 보장합니다. 모달 컨트롤러의 경우 dismiss가 사용됩니다. 코드는 상태 검사가 필요하지 않습니다. UISwipeGestureRecognizer는 이산적이며 성공적인 인식 시에만 액션을 호출합니다.
스와이프 방향을 결정하고 onSwipeLeft/Right 콜백을 호출하는 간단한 GestureDetector 래퍼를 구현합니다.
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() } }).
테이블 셀을 왼쪽으로 스와이프할 때 삭제 버튼을 추가합니다. 수동 제스처 인식 없이 내장 iOS 11+ API를 사용합니다.
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
}
메서드는 UIContextualAction 배열과 함께 UISwipeActionsConfiguration을 반환합니다. style: .destructive는 자동으로 빨간색 배경을 설정합니다. performsFirstActionWithFullSwipe = true는 추가 탭 없이 전체 스와이프로 액션을 실행할 수 있게 합니다. IT Sectr에서는 액션이 있는 모든 리스트에 이 API를 사용합니다. 표준화되어 있고 VoiceOver를 지원하며 사용자 정의 GestureRecognizer가 필요하지 않습니다.
자주 묻는 질문
스와이프는 빠른 움직임과 손가락 떼기가 있는 이산적 제스처이고, 패닝은 좌표 추적이 있는 연속적 드래그입니다. UISwipeGestureRecognizer는 한 번 실행되고 UIPanGestureRecognizer는 .began → .changed → .ended를 생성합니다. 선택은 시나리오에 따라 다릅니다. 스와이프는 액션(삭제, 넘기기)용, pan은 이동(지도, 슬라이더)용입니다.
제스처 로직은 동일하지만 API가 다릅니다. iOS는 direction과 함께 UISwipeGestureRecognizer를 사용합니다. Android는 사용자 정의 임계값으로 GestureDetector.onFling()을 사용합니다. 결과는 다른 구현에서 동일한 UX입니다. 크로스 플랫폼 프레임워크(Flutter, React Native)는 통합 스와이프 API를 통해 차이를 추상화합니다.
iOS(11+)에서는 UITableViewDelegate에서 UISwipeActionsConfiguration을 사용합니다. Android에서는 RecyclerView에 ItemTouchHelper.SimpleCallback을 사용합니다. 이러한 API는 수동 GestureRecognizer를 대체하고 액션 버튼과 애니메이션을 지원하며 스크롤 충돌을 올바르게 처리하고 플랫폼 HIG에 적응합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.