How it works
GestureState is a property wrapper that holds a value tied to the lifetime of an in-progress gesture. Unlike @State, the value it stores is transient: SwiftUI tracks the gesture for you, and the moment the gesture ends or is cancelled it automatically resets the property to its initial value. Reach for it when you want a view to reflect the live, ephemeral state of a drag, press, or magnification — an offset that follows a finger, or a highlight that should vanish the instant the user lets go — without writing any teardown code yourself.
Declare the transient value with @GestureState
Apply the wrapper to a stored property and give it an initial value that represents the resting state. SwiftUI keeps the property equal to this value except while the gesture is active. In the example,
@GestureState private var isPressed = falsedefines a Boolean that isfalsewhenever no press is underway.Project the binding with the $ prefix
The wrapper exposes a projected value through the
$syntax, which produces the handle a gesture needs to mutate the transient state. Here$isPressedis passed into the gesture so SwiftUI knows which GestureState property to drive as the interaction progresses.Drive it from a gesture with updating(_:body:)
The
updating(_:body:)modifier connects a gesture to a GestureState binding and runs a closure on every value change. The closure receives the gesture'scurrentvalue, aninout stateyou write to, and a transaction. In the example,LongPressGesture(minimumDuration: 0.5).updating($isPressed) { current, state, _ in state = current }copies the press's progress intoisPressedas long as the finger is held down.Rely on the automatic reset
Because GestureState is owned by the gesture, you never reset it manually — when the gesture completes or is interrupted, SwiftUI restores the initial value. That is why
isPressedsnaps back tofalseon release with no extra code, returning the view to its idle appearance.Read it like any view state
Inside
body, the wrapped value is read directly to derive the UI, and changes flow through SwiftUI's normal update mechanism. The example readsisPressedto choose the fill (Color.greenvsColor.blue), thescaleEffect, and theText(isPressed ? "Pressed" : "Idle")label so the circle visibly responds while the press lasts.
state = current to state = !current and watch the reset still kick in on release — the circle stays idle during the press and only the GestureState lifetime, not your logic, returns it to false.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 GestureStateDemo: View {
@GestureState private var isPressed = false
var body: some View {
VStack(spacing: 16) {
Text("Press and hold the circle")
.font(.headline)
Circle()
.fill(isPressed ? Color.green : Color.blue)
.frame(width: 100, height: 100)
.scaleEffect(isPressed ? 1.2 : 1.0)
.gesture(
LongPressGesture(minimumDuration: 0.5)
.updating($isPressed) { current, state, _ in
state = current
}
)
Text(isPressed ? "Pressed" : "Idle")
.foregroundStyle(.secondary)
}
.padding()
}
}