Observation on SwiftUI+DragGesture: GestureState is already reset when OnEnded starts

When I set up a dragGesture using a gestureState, the gestureState has the correct value during the onChange phase. But when the onEnded phase is entered, the gesturePhase is reset to its original default value.

Is this the correct behavior?

I would like the gestureState to retain its value since I want to make use of it in the onEnded phase. After the onEnded phase is completed, then the gestureState should be reset to its original value.

Replies

I may have been misusing what GestureState was intended for.

I came up with another approach that seems easier for what I want.

This is what I want:

  • I have a SpriteView with a GameScene (subclassed from SKScene).

  • I want the SpriteView to handle the gestures (since it is way easier than the old approach).

  • I want the tapped buttonNode in the GameScene to track the location for highlighting and unhighlighting.

  • When the tracking ends, I want the tapped buttonNode to do its action if it is still being selected.

  • But I cannot attach the SwiftUI gestures to the SKNode buttonNodes of the GameScene since they are not Views.

  • Thus, I have to have a custom SwiftUI dragGesture to handle the above.

  • The major problem is to remember the originally touched buttonNode.

  • Instead of trying to use GestureState, I just use a locally captured variable within the "var" or "function" gesture.

Here is a modified version of what I do. The modification uses force unwrapping to keep it simple.

I put the dragGesture as part of the GameScene rather than as part of the file with the SpriteView. This is because of the needs for my particular program. The code can be simplified if you put it with the file with the SpriteView. For example, you can eliminate the guard statements for gameScene.

The major point of the example is to how to capture the "begin" state of the gesture without using GestureState.


extension GameScene {

    var buttonNodeGesture: some Gesture {
        var hitButtonNode:Button3Node?
        return DragGesture(minimumDistance: 0)
            .onChanged { [weak self] gesture in
                guard let gameScene = self else { return }
                let buttonNodes = gameScene.buttonNodes
                let firstButtonNode = buttonNodes.first!
                let parent = firstButtonNode.parent!
                do { // updating stuff
                    if hitButtonNode == nil {
                        let startLocation = gameScene.convert(fromGesture: gesture.startLocation, to: parent)
                        hitButtonNode = gameScene.buttonNodes.filter{$0.contains(startLocation)}.first
                    }
                }
                do { // onChanged stuff
                    if let hitButtonNode = hitButtonNode {
                        let location = gameScene.convert(fromGesture: gesture.location, to: parent)
                        if hitButtonNode.contains(location) {
                            hitButtonNode.highlight()
                        }
                        else {
                            hitButtonNode.unhighlight()
                        }
                    }
                }
            }
            .onEnded { [weak self] gesture in
                guard let gameScene = self else { return }
                let buttonNodes = gameScene.buttonNodes
                let firstButtonNode = buttonNodes.first!
                let parent = firstButtonNode.parent!
                do { // onEnded stuff
                    if let hitButtonNode = hitButtonNode {
                        hitButtonNode.unhighlight()
                        let location = gameScene.convert(fromGesture: gesture.location, to: parent)
                        if hitButtonNode.contains(location) {
                            hitButtonNode.doButtonAction()
                        }
                    }
                }
                hitButtonNode = nil // Note: probably not needed
            }
    }
}

In the given example, the last line is needed (hitButtonNode = nil). This could be a source of error, especially when there are many items to reset.

Since GestureState automatically handles the resetting of the state, it would be nice if it wasn't reset before onEnded is started (this is my original note). I am not sure why it is being reset before .onEnded starts. My surmise is that Apple intends the gestureState to be used to change the appearance of some other item within the view (like its highlighting or its color). But, I don't see why this would require the gestureState to be reset before beginning .onEnded.

This last comment leads to another comment about .onChange and .onEnded for gestures. It is not clear to me whether the mouseUp event that triggers .onEnded would first call .onChange and then call .onEnded. It would be nice if it was documented (and actually done) this way. Since I am not sure, I usually make the code in .onChange a single function and when .onEnded starts I call that function to insure that I am following the last mouse or tap event.