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.
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.0and@State private var offset: CGSize = .zeroare the live transform that the gestures write into and the view renders from.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)andoffset(offset)make theRoundedRectanglereflect the current scale and position; every state update from a gesture re-runs these and redraws.Attach a DragGesture to update position
A
.gesturecarrying aDragGesturefeeds 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.Attach a MagnificationGesture to update scale
A second
.gesturecarrying aMagnificationGesturehandles pinch-to-zoom in the same stream-and-settle pattern..onChanged { scale = $0 }binds the live magnification factor toscale, and.onEnded { _ in scale = 1.0 }restores the original size on release.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.
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.
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()
}
}