Home > Back-end >  SwiftUI - how to exclude inner `DragGesture` from outer `simultaneousGesture`?
SwiftUI - how to exclude inner `DragGesture` from outer `simultaneousGesture`?

Time:12-05

I have a yellow button and a blue circle inside a gray container. I added The entire container moves The container moves ✅
The entire container moves The container moves ❌
The entire container moves, but only the circle should move

Here's my code:

struct ContentView: View {
    @GestureState var containerOffset = CGSize.zero
    @GestureState var circleOffset = CGSize.zero
    
    var body: some View {
        
        VStack { /// gray container
            
            /// should move the gray container when dragged
            Button {
                print("Button Pressed")
            } label: {
                Text("Button")
                    .padding(50)
                    .background(.yellow)
            }
            
            /// should **NOT** move the gray container when dragged
            /// How do I make it move the circle instead?
            Circle()
                .fill(.blue)
                .frame(width: 100, height: 100)
                .offset(circleOffset)
                .gesture(
                    DragGesture() /// this never gets called!
                        .updating($circleOffset) { value, circleOffset, transaction in
                            circleOffset = value.translation
                        }
                )
                .padding(50)
        }
        .background(Color.gray)
        .offset(containerOffset)
        .simultaneousGesture( /// `simultaneous` is needed to allow dragging from the yellow button
            DragGesture() /// move the gray container
                .updating($containerOffset) { value, containerOffset, transaction in
                    containerOffset = value.translation
                }
        )
    }
}

How do I stop the outer DragGesture from swallowing the inner circle's DragGesture?

CodePudding user response:

One way to do it is to add a state variable in ContentView that tracks whether the circle's gesture is active. When the circle's gesture is active, use a GestureMask of .subviews on the outer gesture to prevent it from activating. You can do it with an @State:

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    @GestureState var containerOffset = CGSize.zero
    @GestureState var circleOffset = CGSize.zero
    @State var subviewDragIsActive = false
    
    var body: some View {
        
        VStack { /// gray container
            
            /// should move the gray container when dragged
            Button {
                print("Button Pressed")
            } label: {
                Text("Button")
                    .padding(50)
                    .background(Color.yellow)
            }
            
            /// should **NOT** move the gray container when dragged
            /// How do I make it move the circle instead?
            Circle()
                .fill(Color.blue)
                .frame(width: 100, height: 100)
                .offset(circleOffset)
                .gesture(
                    DragGesture() /// this never gets called!
                        .updating($circleOffset) { value, circleOffset, transaction in
                            circleOffset = value.translation
                        }
                        .onChanged { _ in
                            subviewDragIsActive = true
                        }
                        .onEnded { _ in
                            subviewDragIsActive = false
                        }
                )
                .padding(50)
        }
        .background(Color.gray)
        .offset(containerOffset)
        .simultaneousGesture( /// `simultaneous` is needed to allow dragging from the yellow button
            DragGesture() /// move the gray container
                .updating($containerOffset) { value, containerOffset, transaction in
                    containerOffset = value.translation
                },
            including: subviewDragIsActive ? .subviews : .all
        )
    }
}

PlaygroundPage.current.setLiveView(ContentView())

or you can do it with an @GestureState:

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    @GestureState var containerOffset = CGSize.zero
    @GestureState var circleOffset = CGSize.zero
    @GestureState var subviewDragIsActive = false
    
    var body: some View {
        
        VStack { /// gray container
            
            /// should move the gray container when dragged
            Button {
                print("Button Pressed")
            } label: {
                Text("Button")
                    .padding(50)
                    .background(Color.yellow)
            }
            
            /// should **NOT** move the gray container when dragged
            /// How do I make it move the circle instead?
            Circle()
                .fill(Color.blue)
                .frame(width: 100, height: 100)
                .offset(circleOffset)
                .gesture(
                    DragGesture() /// this never gets called!
                        .updating($circleOffset) { value, circleOffset, transaction in
                            circleOffset = value.translation
                        }
                        .updating($subviewDragIsActive) {
                            _, flag, _ in
                            flag = true
                        }
                )
                .padding(50)
        }
        .background(Color.gray)
        .offset(containerOffset)
        .simultaneousGesture( /// `simultaneous` is needed to allow dragging from the yellow button
            DragGesture() /// move the gray container
                .updating($containerOffset) { value, containerOffset, transaction in
                    containerOffset = value.translation
                },
            including: subviewDragIsActive ? .subviews : .all
        )
    }
}

PlaygroundPage.current.setLiveView(ContentView())

If the circle is in a separately-defined View type, you should use an @State so you can pass down a Binding, like this:

import SwiftUI
import PlaygroundSupport

struct CircleView: View {
    @GestureState var circleOffset = CGSize.zero
    @Binding var dragIsActive: Bool
    
    var body: some View {
        Circle()
            .fill(Color.blue)
            .frame(width: 100, height: 100)
            .offset(circleOffset)
            .gesture(
                DragGesture() /// this never gets called!
                    .updating($circleOffset) { value, circleOffset, transaction in
                        circleOffset = value.translation
                    }
                    .onChanged { _ in dragIsActive = true }
                    .onEnded { _ in dragIsActive = false }
                    )
    }
}

struct ContentView: View {
    @GestureState var containerOffset = CGSize.zero
    @State var subviewDragIsActive = false
    
    var body: some View {
        
        VStack { /// gray container
            
            /// should move the gray container when dragged
            Button {
                print("Button Pressed")
            } label: {
                Text("Button")
                    .padding(50)
                    .background(Color.yellow)
            }
            
            /// should **NOT** move the gray container when dragged
            /// How do I make it move the circle instead?
            CircleView(dragIsActive: $subviewDragIsActive)
                .padding(50)
        }
        .background(Color.gray)
        .offset(containerOffset)
        .simultaneousGesture( /// `simultaneous` is needed to allow dragging from the yellow button
            DragGesture() /// move the gray container
                .updating($containerOffset) { value, containerOffset, transaction in
                    containerOffset = value.translation
                },
            including: subviewDragIsActive ? .subviews : .all
        )
    }
}

PlaygroundPage.current.setLiveView(ContentView())

If any of several subviews might have a drag going on simultaneously (due to multitouch), you should probably use the preference system to pass their isActive states up to ContentView, but that's a significantly more complex implementation.

  • Related