TechnologiesSwiftUI

ManipulableResponderModifier struct

iOSmacOStvOSwatchOSvisionOS✓ renders

How it works

ManipulableResponderModifier is the view modifier that lets a view respond to direct-manipulation gestures — dragging and pinching — by translating those continuous touch events into state changes you can drive your layout from. SwiftUI applies it when you attach interactive gestures to a view, wiring the gesture's value stream into your own bindings so the view can move, scale, and settle in response. Reach for it whenever a view should feel grabbable: a card the user repositions, an image they zoom, or any element whose transform should track the gesture in real time and animate back to rest when the interaction ends.

  1. Hold transform state with @State

    The modifier reads from values you own, so declare the properties it manipulates. Here @State private var scale: CGFloat = 1.0 and @State private var offset: CGSize = .zero are the live transform that the gestures write into and the view renders from.

  2. Project state onto the view's transform

    Apply the state to the rendered view before attaching gestures, so manipulation has something visible to affect. scaleEffect(scale) and offset(offset) make the RoundedRectangle reflect the current scale and position; every state update from a gesture re-runs these and redraws.

  3. Attach a DragGesture to update position

    A .gesture carrying a DragGesture feeds continuous translation into the responder. .onChanged { offset = $0.translation } tracks the finger as it moves, while .onEnded { _ in offset = .zero } returns the view to rest when the drag lifts.

  4. Attach a MagnificationGesture to update scale

    A second .gesture carrying a MagnificationGesture handles pinch-to-zoom in the same stream-and-settle pattern. .onChanged { scale = $0 } binds the live magnification factor to scale, and .onEnded { _ in scale = 1.0 } restores the original size on release.

  5. Smooth the settle with animation

    Pair the responder with an implicit animation so the snap-back reads as motion rather than a jump. .animation(.spring, value: offset) interpolates the offset whenever it changes, giving the released card a springy return to .zero.

Try it — In the DragGesture's .onEnded closure, replace offset = .zero with a fixed value like offset = CGSize(width: 60, height: 0) to watch the card settle at a new resting position instead of snapping back to center.

Example & preview

Press Run live & edit to compile it in your browser — then edit the Swift on the left and the preview re-renders live.

ManipulableResponderModifier.swift
struct ManipulableResponderModifierDemo: View {
    @State private var scale: CGFloat = 1.0
    @State private var offset: CGSize = .zero

    var body: some View {
        VStack(spacing: 16) {
            Text("Drag and pinch the card")
                .font(.headline)

            RoundedRectangle(cornerRadius: 16)
                .fill(.blue.gradient)
                .frame(width: 160, height: 100)
                .overlay(Text("Manipulable").foregroundStyle(.white))
                .scaleEffect(scale)
                .offset(offset)
                .gesture(
                    DragGesture()
                        .onChanged { offset = $0.translation }
                        .onEnded { _ in offset = .zero }
                )
                .gesture(
                    MagnificationGesture()
                        .onChanged { scale = $0 }
                        .onEnded { _ in scale = 1.0 }
                )
                .animation(.spring, value: offset)
        }
        .padding()
    }
}
Live preview
Drag and pinch the card Manipulable
Drag and pinch the card Manipulable
swift → lexer → parser → sema → uiir → canvas Open in Studio ↗
What's new in SwiftUI 27 →