SimultaneousGesture.updating(_:body:) does not always reset

I've got another issue with Gesture.updating(_:body:).. This looks a bit like this thread, but it's different. My problem occurs when creating a simultaneous gesture combining a RotateGesture with a MagnifyGesture.

struct ManipulationState: Equatable {
    var magnification: CGFloat?
    var rotation: Angle?
}

@GestureState var state: ManipulationState? = nil

var gesture: GestureStateGesture<SimultaneousGesture<RotateGesture, MagnifyGesture>, ManipulationState?> {
    SimultaneousGesture(
        RotateGesture(minimumAngleDelta: .degrees(5)),
        MagnifyGesture(minimumScaleDelta: 0.1)
    )
        .updating($state) { value, state, transaction in
            state = ManipulationState(
                magnification: value.second?.magnification,
                rotation: value.first?.rotation
            )
        }
}

When rotating and magnifying quite a bit, at some point, something happens, the gesture stops working altogether and the state never resets to the initial value any more.

When adding an onChange handler to some view in the body, you can watch state never gets nil again.

.onChange(of: state, { oldValue, newValue in
    print("\(oldValue) \(newValue)")
})

I noticed the same happening when using ExclusiveGesture instead of SimultaneousGesture.

Full code example here: https://github.com/teameh/GestureIssueTester/blob/main/GestureTester/ContentView.swift

Video demonstrating issue here: https://github.com/teameh/GestureIssueTester/raw/refs/heads/main/screen-recording.mov

I've tried using Gesture.onChanged(_:) and Gesture.onEnded(_:)as well, butonEnded` is also not always called.

Is there anyone here who ran into the same problem or perhaps someone can share tips on a workaround or a fix?

Tested using Xcode 16.0 (16A242d)

Answered by DTS Engineer in 809784022

Since GestureState resets its property back to its initial state when the gesture ends, you should provide an initial state instead of nil.

Also Gesture.onChanged(_:) and Gesture.onEnded(_:) introduces a new type which conflicts with the type specified : var gesture: GestureStateGesture<SimultaneousGesture<RotateGesture, MagnifyGesture>, ManipulationState?>

You should try this instead:

struct Model: Equatable {
    var rotation: Angle = .zero
    var magnification: CGFloat = 1.0
}

struct ContentView: View {
    @GestureState private var gestureState = Model()
    
    var simultaneousGesture: some Gesture {
        SimultaneousGesture(
            RotateGesture(minimumAngleDelta: .degrees(5)),
            MagnifyGesture(minimumScaleDelta: 0.1)
        )
        .updating($gestureState) { value, state, _ in
            if let rotation = value.first {
                state.rotation = rotation.rotation
            }
            if let magnification = value.second {
                state.magnification = magnification.magnification
            }
        }
        .onChanged { value in
            if let rotation = value.first?.rotation {
                print("Rotation onChange: \(rotation.degrees) degrees")
            }
            if let magnification = value.second?.magnification {
                print("Magnification onChange: \(magnification)")
            }
            
        }
        .onEnded { value in
            if let rotation = value.first?.rotation {
                print("Rotation ended at: \(rotation.degrees) degrees")
            }
            if let magnification = value.second?.magnification {
                print("Magnification ended at: \(magnification)")
            }
        }
    }

    var body: some View {
        Rectangle()
            .fill(Color.orange)
            .frame(width: 200, height: 200)
            .shadow(radius: 10)
            .scaleEffect(gestureState.magnification)
            .rotationEffect(gestureState.rotation)
            .gesture(simultaneousGesture)
            .onChange(of: gestureState) {
                print(gestureState)
            }
    }
}

Since GestureState resets its property back to its initial state when the gesture ends, you should provide an initial state instead of nil.

Also Gesture.onChanged(_:) and Gesture.onEnded(_:) introduces a new type which conflicts with the type specified : var gesture: GestureStateGesture<SimultaneousGesture<RotateGesture, MagnifyGesture>, ManipulationState?>

You should try this instead:

struct Model: Equatable {
    var rotation: Angle = .zero
    var magnification: CGFloat = 1.0
}

struct ContentView: View {
    @GestureState private var gestureState = Model()
    
    var simultaneousGesture: some Gesture {
        SimultaneousGesture(
            RotateGesture(minimumAngleDelta: .degrees(5)),
            MagnifyGesture(minimumScaleDelta: 0.1)
        )
        .updating($gestureState) { value, state, _ in
            if let rotation = value.first {
                state.rotation = rotation.rotation
            }
            if let magnification = value.second {
                state.magnification = magnification.magnification
            }
        }
        .onChanged { value in
            if let rotation = value.first?.rotation {
                print("Rotation onChange: \(rotation.degrees) degrees")
            }
            if let magnification = value.second?.magnification {
                print("Magnification onChange: \(magnification)")
            }
            
        }
        .onEnded { value in
            if let rotation = value.first?.rotation {
                print("Rotation ended at: \(rotation.degrees) degrees")
            }
            if let magnification = value.second?.magnification {
                print("Magnification ended at: \(magnification)")
            }
        }
    }

    var body: some View {
        Rectangle()
            .fill(Color.orange)
            .frame(width: 200, height: 200)
            .shadow(radius: 10)
            .scaleEffect(gestureState.magnification)
            .rotationEffect(gestureState.rotation)
            .gesture(simultaneousGesture)
            .onChange(of: gestureState) {
                print(gestureState)
            }
    }
}

Thank you for the detailed response and example. However, I've noticed an unexpected behavior in the implementation.

The expected behavior of this code looks like "normal output" from this file: https://github.com/teameh/GestureIssueTester/blob/engineer-example/output.txt

This shows the expected flow: state changes during gesture updates, followed by gesture completion and state reset.

But if you try this example out, and mess a bit with the gestures, within a minute, the console will look like this and everything will get stuck.

See "error ouput" in this file: https://github.com/teameh/GestureIssueTester/blob/engineer-example/output.txt

Two key issues are apparent:

  • The state no longer resets to initial values
  • The onEnded handler stops being called

I've recorded this happening in this video. https://github.com/teameh/GestureIssueTester/raw/refs/heads/engineer-example/screen-recording-engineer-example.mov

I'm just activating the gestures and letting them end a couple of times. In the video, after 3 seconds everything gets stuck and nothing is printed any more, even when you keep trying to activate the gesture.

I had the same issue, and I managed to work around it by setting minimumAngleDelta and minimumScaleDelta to .zero. It seems like the problem occurs when one gesture ends while the other is waiting for the minimal delta, causing things to get stuck. If you need to use non-zero deltas, you can handle the threshold check explicitly in the updating handler to ensure gestures don't get stuck.

SimultaneousGesture.updating(_:body:) does not always reset
 
 
Q