Skip to content

SwiftUI Notes 2: Gestures

01/06/2026

Gestures are how SwiftUI detects what the user's fingers are doing. You attach one to any view, SwiftUI gives you data about the interaction, and you decide what to do with it. Every gesture follows the same three-part pattern: a @State variable, a visual modifier, and a .gesture() that ties them together.

I. The Universal Pattern

Every gesture example in SwiftUI follows the same recipe. Once you see it, new gestures become predictable — the only things that change are which gesture, which state type, and which modifier.

  • `@State` variable — stores the gesture data (offset, scale, angle, toggle).
  • Visual modifier — reads from that variable (.offset, .scaleEffect, .rotationEffect).
  • `.gesture()` — writes to the variable on .onChanged and resets on .onEnded.

The lifecycle

Two events drive every gesture:

  • `.onChanged` — fires repeatedly while the gesture is in progress. Tells you what's happening right now (position, distance, scale).
  • `.onEnded` — fires once when the finger lifts. Time to clean up or animate back.

Simple gestures like TapGesture only have .onEnded — there's nothing meaningful to track mid-tap.

II. The Five Gesture Types

GestureDetectsData you get
TapGestureQuick tap (single, double, triple)Just the event
LongPressGesturePress and holdWhether threshold was met
DragGestureFinger sliding across screentranslation, location, startLocation
MagnifyGesturePinch to zoomScale factor (2.0 = doubled)
RotateGestureTwo-finger twistRotation angle

III. Code Examples

1. TapGesture

Detect a tap and react. Toggle a color, show a sheet, trigger any action. No .onChanged — a tap is instant.

@State var tapped = false

Circle()
    .fill(tapped ? Color.green : Color.red)
    .frame(width: 100, height: 100)
    .gesture(
        TapGesture()
            .onEnded {
                tapped.toggle()
            }
    )

Tap the circle and it flips between red and green.

2. LongPressGesture

Fires after the user holds for a set time. Use minimumDuration to control how long they need to hold.

@State var isPressed = false

RoundedRectangle(cornerRadius: 16)
    .fill(isPressed ? Color.orange : Color.blue)
    .frame(width: 150, height: 60)
    .scaleEffect(isPressed ? 1.2 : 1.0)
    .gesture(
        LongPressGesture(minimumDuration: 0.8)
            .onEnded { _ in
                withAnimation(.spring()) {
                    isPressed.toggle()
                }
            }
    )

Hold for 0.8 seconds — the rectangle scales up and changes color with a spring animation.

3. DragGesture

Tracks finger movement in real time. Gives you a CGSize translation — how far the finger moved from the starting point.

@State var dragOffset: CGSize = .zero

RoundedRectangle(cornerRadius: 24)
    .fill(Color.indigo)
    .frame(width: 100, height: 100)
    .offset(x: dragOffset.width, y: dragOffset.height)
    .gesture(
        DragGesture()
            .onChanged { value in
                dragOffset = value.translation
            }
            .onEnded { _ in
                withAnimation(.spring()) {
                    dragOffset = .zero
                }
            }
    )

.onChanged fires every frame while dragging — updates position in real time. .onEnded resets to .zero — the square springs back home.

4. MagnifyGesture

Detects a pinch. Gives you a scale multiplier — 1.0 is normal, 2.0 is double, 0.5 is half.

@State var scale: CGFloat = 1.0

Image(systemName: "star.fill")
    .font(.system(size: 80))
    .foregroundColor(.yellow)
    .scaleEffect(scale)
    .gesture(
        MagnifyGesture()
            .onChanged { value in
                scale = value.magnification
            }
            .onEnded { _ in
                withAnimation { scale = 1.0 }
            }
    )

Pinch to grow or shrink the star. Let go and it bounces back to normal.

5. RotateGesture

Detects two fingers twisting. Returns a rotation Angle.

@State var angle: Angle = .zero

Rectangle()
    .fill(Color.mint)
    .frame(width: 120, height: 120)
    .rotationEffect(angle)
    .gesture(
        RotateGesture()
            .onChanged { value in
                angle = value.rotation
            }
            .onEnded { _ in
                withAnimation { angle = .zero }
            }
    )

Twist two fingers on the square. Release to spring back to original rotation.

IV. Key Concepts

CGSize

A Core Graphics struct holding two numbers: width and height. In drag gestures, it stores how far the finger moved — width for horizontal, height for vertical. CGSize.zero (or just .zero) means no movement, and is used both as the starting value and the reset value.

withAnimation

Wrapping a state change in withAnimation makes the transition smooth. Without it, the view snaps instantly. With it, SwiftUI interpolates between the old and new values.

  • .spring() for bouncy
  • .easeInOut for smooth
  • .linear for constant speed

Gestures don't move views

Gestures only report data — they don't change anything visual on their own. You take the data and apply it through modifiers like .offset(), .scaleEffect(), or .rotationEffect(). The gesture is the sensor; the modifier is the actuator.