Conditional Gestures in SwiftUI

Working with gestures in swiftUI is an essential part of any app and can add tremendous variety in user interaction. Unfortunately sometimes gestures can interfere with each other causing issues and unwanted behavior. Luckily, swiftUI is heavily reliant on conditional modifiers, and these modifiers can be applied to gestures as well.


First, lets start with a simple button and a shape we can modify:

VStack {
            Button(action: {
                
            }, label: {
                // styling for the button
                Text("Toggle")
                    .font(.title)
            })
            // view that has gesture
            Circle()
                .frame(width: 50, height: 50)
                .foregroundColor(Color.red.opacity(0.75))

Next, add the variables we need, and apply the gesture to the circle.

// the value that portrays the offset
    @State var myOffset: CGSize = .zero
    
    // intermediate value to help with remebering positioning
    @State var myNewOffset: CGSize = .zero

Circle()
                .frame(width: 50, height: 50)
                .foregroundColor(Color.red.opacity(0.75))
                // allow the circle to be moved when dragged
                . offset(x: self.myOffset.width, y: self.myOffset.height)
                .gesture(
                    // use a conditional modifier to control the gesture
                    DragGesture()
                        // when the user is interacting with the view
                        .onChanged { value in
                            // set the offset to follow the users finger, accounting for if they have already dragged or not
                            self.myOffset.width = value.translation.width + self.myNewOffset.width
                            self.myOffset.height = value.translation.height + self.myNewOffset.height
                    }
                    // when the user stops dragging, store the current location of the view
                    .onEnded { value in
                        self.myNewOffset = self.myOffset
                    }
                )

Then finally, create a State variable to control the gesture, add toggle-ability to the button, and then configure the gesture code:

// control the gesture state
    @State var gestureToggle: Bool = false

Button(action: {
                // toggle on or off the gesture
                self.gestureToggle.toggle()
            }, label: {
                // styling for the button
                Text("Toggle")
                    .font(.title)

// view that has gesture
            Circle()
                .frame(width: 50, height: 50)
                .foregroundColor(Color.red.opacity(0.75))
                // allow the circle to be moved when dragged
                . offset(x: self.myOffset.width, y: self.myOffset.height)
                .gesture(
                    // use a conditional modifier to control the gesture
                    !self.gestureToggle ? // specify gesture type
                    DragGesture()
                        // when the user is interacting with the view
                        .onChanged { value in
                            // set the offset to follow the users finger, accounting for if they have already dragged or not
                            self.myOffset.width = value.translation.width + self.myNewOffset.width
                            self.myOffset.height = value.translation.height + self.myNewOffset.height
                    }
                    // when the user stops dragging, store the current location of the view
                    .onEnded { value in
                        self.myNewOffset = self.myOffset
                    }
                    // when the modifer is true, nil the gesture
                    : nil
                )

And there it is! The circle will be drag-able at first, then once the button is clicked the dragging will be disabled.

Final Code:

struct ContentView: View {
    // control the gesture state
    @State var gestureToggle: Bool = false
    
    // the value that portrays the offset
    @State var myOffset: CGSize = .zero
    
    // intermediate value to help with remebering positioning
    @State var myNewOffset: CGSize = .zero
    
    var body: some View {
        // view stack
        VStack {
            Button(action: {
                // toggle on or off the gesture
                self.gestureToggle.toggle()
            }, label: {
                // styling for the button
                Text("Toggle")
                    .font(.title)
            })
            // view that has gesture
            Circle()
                .frame(width: 50, height: 50)
                .foregroundColor(Color.red.opacity(0.75))
                // allow the circle to be moved when dragged
                . offset(x: self.myOffset.width, y: self.myOffset.height)
                .gesture(
                    // use a conditional modifier to control the gesture
                    !self.gestureToggle ? // specify gesture type
                    DragGesture()
                        // when the user is interacting with the view
                        .onChanged { value in
                            // set the offset to follow the users finger, accounting for if they have already dragged or not
                            self.myOffset.width = value.translation.width + self.myNewOffset.width
                            self.myOffset.height = value.translation.height + self.myNewOffset.height
                    }
                    // when the user stops dragging, store the current location of the view
                    .onEnded { value in
                        self.myNewOffset = self.myOffset
                    }
                    // when the modifer is true, nil the gesture
                    : nil
                )
        }
        .padding()
    }
}

Source Code: https://drive.google.com/drive/folders/1YU2H_8lKLLhjSL4yG7MUffKOj_7KFggX?usp=sharing

Leave a Reply

Your email address will not be published. Required fields are marked *