🔋

Fixing Gesture Interruptions: @GestureState vs @State

(Updated on )

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:

Swift
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:

Swift
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 @State Flaw: System interruptions do not trigger the onEnded closure. If your reset logic relies solely on onEnded, the isPressed variable will remain stuck at true, causing your UI to freeze in an interactive state.
  • The @GestureState Advantage: 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 @State for a gesture (e.g., you need to persist the final position of a drag), you may need to monitor ScenePhase changes or strictly handle cancellations to manually reset your state when the app resigns activity.

Summary: How to Choose

  1. 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.
  2. 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.

Further Reading

Related Tips

Subscribe to Fatbobman

Weekly Swift & SwiftUI highlights. Join developers.

Subscribe Now