SwiftUI - 如何从外部 `simultaneousGesture` 中排除内部 `DragGesture`?

SwiftUI - how to exclude inner `DragGesture` from outer `simultaneousGesture`?

我在灰色容器内有一个黄色按钮和一个蓝色圆圈。我将 DragGestures 添加到圆圈和容器中 — 然而,圆圈永远不会移动。

这是我想要的行为:

Dragging the gray area Dragging the button Dragging the circle
The container moves The container moves The circle moves

这是我得到的:

Dragging the gray area Dragging the button Dragging the circle
The container moves ✅
The container moves ✅
The container moves ❌

这是我的代码:

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
                }
        )
    }
}

如何阻止外圈 DragGesture 吞噬内圈的 DragGesture

一种方法是在 ContentView 中添加一个状态变量来跟踪圆圈的手势是否处于活动状态。当圆圈的手势处于活动状态时,在外部手势上使用 GestureMask.subviews 以防止其激活。您可以使用 @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())

或者您可以使用 @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())

如果圆是单独定义的 View 类型,你应该使用 @State 这样你就可以传递 Binding,像这样:

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())

如果多个子视图中的任何一个可能同时进行拖动(由于多点触控),您可能应该使用首选项系统将它们的 isActive 状态传递到 ContentView,但这是一个复杂得多的实现.