TL;DR: Have you ever dragged a view, pulled down the Notification Center, and found your view stuck in the middle of the screen, refusing to reset? This happens because the system interrupted the gesture, preventing onEnded from firing. @GestureState is the specific solution designed to handle these transient states and interruption resets automatically.
@GestureState is purpose-built for gesture operations. It automatically resets properties when a gesture is interrupted, making it ideal for transient, gesture-driven state. While @State is more general-purpose, handling gesture cancellations (non-normal completion) with it often requires complex, error-prone boilerplate code.
Background
@GestureState is a property wrapper provided by SwiftUI specifically for gesture tracking. In many scenarios, developers might find that @State seems to achieve similar results. So, why did Apple provide @GestureState? Its core value lies in its automated handling of abnormal terminations (interruptions).
Basic Usage Comparison
Both @GestureState and @State can be used to store and update state, such as implementing click or drag interactions.
Using @GestureState
The following code demonstrates the standard usage of @GestureState paired with the .updating modifier:
struct GestureStateExample: View {
@GestureState var isPressed = false
var body: some View {
Rectangle()
.fill(.orange)
.frame(width: 200, height: 200)
.gesture(
DragGesture(minimumDistance: 0)
.updating($isPressed) { _, state, _ in
state = true
}
)
.overlay(
Text(isPressed ? "Pressing" : "Released")
)
.animation(.easeInOut, value: isPressed)
}
}
In this example, @GestureState updates to true while the gesture is active. As soon as the gesture ends or is interrupted, the state automatically resets to its initial value (false).
Using @State
Similar functionality can be achieved with @State, though it requires more code:
struct StateExample: View {
@State var isPressed = false
var body: some View {
Rectangle()
.fill(.orange)
.frame(width: 200, height: 200)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
isPressed = true
}
.onEnded { _ in
isPressed = false
}
)
.overlay(
Text(isPressed ? "Pressing" : "Released")
)
}
}
On the surface, these two methods appear to behave identically during normal operation.
The Core Difference: Gesture Interruption
Handling gesture interruption is the critical differentiator between @GestureState and @State, and it is a common source of bugs in SwiftUI apps.
What is Gesture Interruption?
A gesture stream is forcibly terminated when a system operation interrupts it. Common examples include:
- Pulling down the Notification Center or Control Center.
- An incoming phone call taking over the screen.
- Triggering a system-level app switching gesture (Home indicator swipe) but not completing it.
When this happens:
- The
@StateFlaw: System interruptions do not trigger theonEndedclosure. If your reset logic relies solely ononEnded, theisPressedvariable will remain stuck attrue, causing your UI to freeze in an interactive state. - The
@GestureStateAdvantage: The system automatically detects the interruption and forces the property to reset to its initial value. No extra code is required.
Visual Comparison
The following video demonstrates the behavioral difference during an interruption:
@GestureState: The state resets automatically after the interruption.@State: The state gets stuck in the “Pressing” mode, requiring manual cleanup logic (which is often tedious to implement).
Note: If you absolutely must use
@Statefor a gesture (e.g., you need to persist the final position of a drag), you may need to monitorScenePhasechanges or strictly handle cancellations to manually reset your state when the app resigns activity.
Summary: How to Choose
- Transient Data? Use @GestureState: For data that is only valid during the gesture, such as drag translation (
Translation), pressing state (Pressing), or magnification scale, always prefer@GestureState. - Persistent Data? Use @State: If you need to save the result after the gesture ends (e.g., dropping a card at a new coordinate), use
@State. However, be vigilant about handling cancellation scenarios.