SwiftUI Gestures
Gestures make your SwiftUI apps interactive and engaging. From simple taps to complex multi-touch interactions, SwiftUI provides a rich set of gesture recognizers. In this guide, you’ll learn how to add gestures to your views and create interactive experiences.
Tap Gesture
Detect single and multiple taps:
struct TapExample: View { @State private var message = "Tap me!"
var body: some View { Text(message) .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(10) .onTapGesture { message = "Tapped!" } }}Double Tap
Text("Double tap me") .onTapGesture(count: 2) { print("Double tapped!") }Triple Tap
Text("Triple tap me") .onTapGesture(count: 3) { print("Triple tapped!") }Long Press Gesture
Detect press and hold:
struct LongPressExample: View { @State private var isPressed = false
var body: some View { Circle() .fill(isPressed ? Color.red : Color.blue) .frame(width: 100, height: 100) .onLongPressGesture { isPressed.toggle() } }}With Minimum Duration
Text("Long press (2 seconds)") .onLongPressGesture(minimumDuration: 2.0) { print("Long pressed for 2 seconds!") }With Progress Tracking
struct LongPressProgress: View { @State private var progress = 0.0 @State private var isComplete = false
var body: some View { Circle() .fill(Color.blue) .frame(width: 100, height: 100) .overlay( Circle() .trim(from: 0, to: progress) .stroke(Color.white, lineWidth: 5) .rotationEffect(.degrees(-90)) ) .onLongPressGesture(minimumDuration: 2.0, pressing: { isPressing in if isPressing { withAnimation(.linear(duration: 2.0)) { progress = 1.0 } } else { progress = 0.0 } }) { isComplete = true } }}Drag Gesture
Track dragging motion:
struct DragExample: View { @State private var offset = CGSize.zero
var body: some View { Circle() .fill(Color.blue) .frame(width: 100, height: 100) .offset(offset) .gesture( DragGesture() .onChanged { value in offset = value.translation } .onEnded { _ in withAnimation { offset = .zero } } ) }}Draggable Card
struct DraggableCard: View { @State private var dragOffset = CGSize.zero @State private var isDragging = false
var body: some View { RoundedRectangle(cornerRadius: 20) .fill(Color.blue) .frame(width: 200, height: 300) .overlay(Text("Drag me").foregroundColor(.white)) .offset(dragOffset) .scaleEffect(isDragging ? 1.1 : 1.0) .shadow(radius: isDragging ? 20 : 5) .gesture( DragGesture() .onChanged { value in dragOffset = value.translation isDragging = true } .onEnded { value in withAnimation(.spring()) { // Snap back if not dragged far enough if abs(value.translation.width) < 100 { dragOffset = .zero } isDragging = false } } ) }}Swipe to Delete
struct SwipeToDelete: View { @State private var offset: CGFloat = 0 @State private var isDeleted = false
var body: some View { if !isDeleted { HStack { Text("Swipe to delete") .padding() Spacer() } .background(Color.white) .offset(x: offset) .background( Color.red .overlay( Image(systemName: "trash") .foregroundColor(.white) .padding(), alignment: .trailing ) ) .gesture( DragGesture() .onChanged { value in if value.translation.width < 0 { offset = value.translation.width } } .onEnded { value in if value.translation.width < -100 { isDeleted = true } else { withAnimation { offset = 0 } } } ) } }}Magnification Gesture
Pinch to zoom:
struct MagnificationExample: View { @State private var scale: CGFloat = 1.0
var body: some View { Image(systemName: "star.fill") .font(.system(size: 50)) .scaleEffect(scale) .gesture( MagnificationGesture() .onChanged { value in scale = value } .onEnded { _ in withAnimation { scale = 1.0 } } ) }}Image Zoom
struct ImageZoom: View { @State private var scale: CGFloat = 1.0 @State private var lastScale: CGFloat = 1.0
var body: some View { Image("photo") .resizable() .scaledToFit() .scaleEffect(scale) .gesture( MagnificationGesture() .onChanged { value in scale = lastScale * value } .onEnded { _ in lastScale = scale // Optional: limit zoom if scale > 3.0 { withAnimation { scale = 3.0 lastScale = 3.0 } } else if scale < 1.0 { withAnimation { scale = 1.0 lastScale = 1.0 } } } ) }}Rotation Gesture
Rotate with two fingers:
struct RotationExample: View { @State private var rotation: Angle = .zero
var body: some View { Rectangle() .fill(Color.blue) .frame(width: 200, height: 100) .rotationEffect(rotation) .gesture( RotationGesture() .onChanged { value in rotation = value } .onEnded { _ in withAnimation { rotation = .zero } } ) }}Persistent Rotation
struct PersistentRotation: View { @State private var rotation: Angle = .zero @State private var lastRotation: Angle = .zero
var body: some View { Image(systemName: "arrow.up") .font(.system(size: 50)) .rotationEffect(rotation) .gesture( RotationGesture() .onChanged { value in rotation = lastRotation + value } .onEnded { _ in lastRotation = rotation } ) }}Combining Gestures
Simultaneously
Both gestures recognized at the same time:
struct SimultaneousGestures: View { @State private var scale: CGFloat = 1.0 @State private var rotation: Angle = .zero
var body: some View { Image(systemName: "star.fill") .font(.system(size: 50)) .scaleEffect(scale) .rotationEffect(rotation) .gesture( MagnificationGesture() .onChanged { value in scale = value } .simultaneously(with: RotationGesture() .onChanged { value in rotation = value } ) ) }}Sequentially
One gesture after another:
struct SequentialGestures: View { @State private var message = "Long press, then drag" @State private var offset = CGSize.zero
var body: some View { Circle() .fill(Color.blue) .frame(width: 100, height: 100) .offset(offset) .gesture( LongPressGesture(minimumDuration: 1.0) .sequenced(before: DragGesture()) .onChanged { value in switch value { case .second(true, let drag): offset = drag?.translation ?? .zero default: break } } .onEnded { _ in withAnimation { offset = .zero } } ) }}Exclusively
Only one gesture at a time:
struct ExclusiveGestures: View { @State private var offset = CGSize.zero
var body: some View { Rectangle() .fill(Color.blue) .frame(width: 200, height: 100) .offset(offset) .gesture( DragGesture() .onChanged { value in offset = value.translation } .exclusively(before: TapGesture() .onEnded { offset = .zero } ) ) }}Gesture State
Track gesture state:
struct GestureStateExample: View { @GestureState private var dragOffset = CGSize.zero @State private var position = CGSize.zero
var body: some View { Circle() .fill(Color.blue) .frame(width: 100, height: 100) .offset(x: position.width + dragOffset.width, y: position.height + dragOffset.height) .gesture( DragGesture() .updating($dragOffset) { value, state, _ in state = value.translation } .onEnded { value in position.width += value.translation.width position.height += value.translation.height } ) }}Practical Gesture Examples
Pull to Refresh
struct PullToRefresh: View { @State private var isRefreshing = false @State private var pullDistance: CGFloat = 0
var body: some View { VStack { if isRefreshing { ProgressView() .padding() }
ScrollView { GeometryReader { geometry in Color.clear.preference( key: ScrollOffsetKey.self, value: geometry.frame(in: .named("scroll")).minY ) } .frame(height: 0)
ForEach(1...20, id: \.self) { item in Text("Item \(item)") .padding() } } .coordinateSpace(name: "scroll") .onPreferenceChange(ScrollOffsetKey.self) { value in if value > 100 && !isRefreshing { isRefreshing = true DispatchQueue.main.asyncAfter(deadline: .now() + 2) { isRefreshing = false } } } } }}
struct ScrollOffsetKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() }}Interactive Card Stack
struct CardStack: View { @State private var cards = ["Card 1", "Card 2", "Card 3"]
var body: some View { ZStack { ForEach(cards, id: \.self) { card in CardView(text: card) .gesture( DragGesture() .onEnded { value in if abs(value.translation.width) > 100 { withAnimation { cards.removeFirst() } } } ) } } }}
struct CardView: View { let text: String
var body: some View { RoundedRectangle(cornerRadius: 20) .fill(Color.blue) .frame(width: 300, height: 400) .overlay(Text(text).foregroundColor(.white)) }}Best Practices
1. Provide Visual Feedback
// ✅ Good - Visual feedback during drag@State private var isDragging = false
Circle() .scaleEffect(isDragging ? 1.2 : 1.0) .gesture( DragGesture() .onChanged { _ in isDragging = true } .onEnded { _ in isDragging = false } )2. Use Animations
// ✅ Good - Smooth animations.onEnded { _ in withAnimation(.spring()) { offset = .zero }}
// ❌ Avoid - Abrupt changes.onEnded { _ in offset = .zero}3. Limit Values
// ✅ Good - Constrained scale.onEnded { _ in if scale > 3.0 { scale = 3.0 } if scale < 0.5 { scale = 0.5 }}4. Reset State Appropriately
// ✅ Good - Clean state management.onEnded { _ in withAnimation { offset = .zero isDragging = false }}Summary
SwiftUI gestures enable rich interactions:
✅ Tap - Single and multiple taps
✅ Long Press - Press and hold
✅ Drag - Swipe and drag motions
✅ Magnification - Pinch to zoom
✅ Rotation - Two-finger rotation
✅ Combining - Simultaneous, sequential, exclusive
Key Takeaways:
- onTapGesture for simple interactions
- DragGesture for swipe and move interactions
- Combine gestures for complex interactions
- Use GestureState for temporary state
- Provide visual feedback during gestures
- Animate gesture state changes
Next Steps: Learn about SwiftUI & UIKit Integration to use UIKit components in SwiftUI! 🚀