在SwiftUI中,如何使手势在容器视图之外无效?

8
我试图使一个视图可以在其剪裁容器视图内拖动和/或缩放(否则它可能会与其他视图的手势冲突),但到目前为止,我所尝试过的方法都没有防止手势超出容器可见边界。
以下是一个简化的演示,展示了我不想要的行为...
当红色矩形部分超出绿色VStack区域(被裁剪)时,它会响应超出绿色区域的拖动手势: Drag not limited by container
import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    
    @State var position: CGPoint = CGPoint(x: 100, y: 150)
    @State var lastPosition: CGPoint = CGPoint(x: 100, y: 150)
    
    var body: some View {
        
        let drag = DragGesture()
        .onChanged {
            self.position = CGPoint(x: $0.translation.width + self.lastPosition.x, y: $0.translation.height + self.lastPosition.y)
        }
        .onEnded {_ in
            self.lastPosition = self.position
        }
        
        return VStack {
            Rectangle().foregroundColor(.red)
                .frame(width: 150, height: 150)
                .position(self.position)
                .gesture(drag)
                .clipped()
        }
        .background(Color.green)
        .frame(width: 200, height: 300)
        
    }
}

PlaygroundPage.current.setLiveView(ContentView())

你如何将此手势限制为仅在容器内(上图中的绿色区域)起作用?
更新: @Asperi的解决方案很好,但是当我在上面添加第二个可拖动容器时,我在第一个容器内部得到了一个“死区”,我无法拖动它(如果第二个正方形不被裁剪,则会覆盖第一个)。问题仅发生在原始/左侧,而不是新的右侧。我认为这与它具有更高优先级有关,因为它是第二个绘制的。
以下是新问题的说明: 2 Containers create dragging dead zone 以下是更新后的代码:
struct ContentView: View {
    
    @State var position1: CGPoint = CGPoint(x: 100, y: 150)
    @State var lastPosition1: CGPoint = CGPoint(x: 100, y: 150)
    let dragArea1: CGRect = CGRect(x: 0, y: 0, width: 200, height: 300)
    
    @State var position2: CGPoint = CGPoint(x: 100, y: 150)
    @State var lastPosition2: CGPoint = CGPoint(x: 100, y: 150)
    let dragArea2: CGRect = CGRect(x: 0, y: 0, width: 200, height: 300)
    
    var body: some View {

        let drag1 = DragGesture(coordinateSpace: .named("dragArea1"))
        .onChanged {
            guard self.dragArea1.contains($0.startLocation) else { return }
            self.position1 = CGPoint(x: $0.translation.width + self.lastPosition1.x, y: $0.translation.height + self.lastPosition1.y)
        }
        .onEnded {_ in
            self.lastPosition1 = self.position1
        }
        
        let drag2 = DragGesture(coordinateSpace: .named("dragArea2"))
        .onChanged {
            guard self.dragArea2.contains($0.startLocation) else { return }
            self.position2 = CGPoint(x: $0.translation.width + self.lastPosition2.x, y: $0.translation.height + self.lastPosition2.y)
        }
        .onEnded {_ in
            self.lastPosition2 = self.position2
        }
        
        return HStack {
            VStack {
                Rectangle().foregroundColor(.red)
                    .frame(width: 150, height: 150)
                    .position(self.position1)
                    .gesture(drag1)
                    .clipped()
            }
            .background(Color.green)
            .frame(width: dragArea1.width, height: dragArea1.height)
            
            VStack {
                Rectangle().foregroundColor(.blue)
                .frame(width: 150, height: 150)
                .position(self.position2)
                .gesture(drag2)
                .clipped()
            }
            .background(Color.yellow)
            .frame(width: dragArea2.width, height: dragArea2.height)
        }
        
    }
}

有什么想法可以在任何容器外保持拖动禁用,就像已经实现的那样,但同时允许在每个容器的完整边界内进行拖动,而不管其他容器发生了什么?

3个回答

5

这里有一个可能的解决方案。想法是在容器坐标空间中拖动坐标,并且如果起始位置超出该命名区域,则忽略拖动。

已在Xcode 11.4 / iOS 13.4上进行了测试

演示

struct ContentView: View {

    @State var position: CGPoint = CGPoint(x: 100, y: 150)
    @State var lastPosition: CGPoint = CGPoint(x: 100, y: 150)

    var body: some View {
        let area = CGRect(x: 0, y: 0, width: 200, height: 300)

        let drag = DragGesture(coordinateSpace: .named("area"))
        .onChanged {
            guard area.contains($0.startLocation) else { return }
            self.position = CGPoint(x: $0.translation.width + self.lastPosition.x, y: $0.translation.height + self.lastPosition.y)
        }
        .onEnded {_ in
            self.lastPosition = self.position
        }

        return VStack {
            Rectangle().foregroundColor(.red)
                .frame(width: 150, height: 150)
                .position(self.position)
                .gesture(drag)
                .clipped()
        }
        .background(Color.green)
        .frame(width: area.size.width, height: area.size.height)
        .coordinateSpace(name: "area")

    }
}

非常感谢你,@Asperi!这是一个非常优雅的解决方案,而且它运行得很好。我已经更新了问题,并提出了一个关于将两个可拖动区域放在一起时出现的问题。请看一下并让我知道是否有任何好的想法来解决这个问题。非常感谢! - rliebert
好的,我已经想出如何完成第二部分了。我会将我的工作作为单独的答案发布。再次感谢@Asperi! - rliebert

3

我两天来一直在寻找类似问题的解决方案。@Asperi 的解决方案有帮助,但对于3个或更多数字并不通用。

我的解决方案:我添加了

.contentShape(Rectangle())

在翻译之前

.gesture(DragGesture().onChanged {

这篇文章对我有所帮助。https://www.hackingwithswift.com/quick-start/swiftui/how-to-control-the-tappable-area-of-a-view-using-contentshape 希望对其他人也有用处。
示例代码:
var body: some View {
        VStack {
            Image("my image")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(200, 200)

                .clipShape(Rectangle())
                .contentShape(Rectangle())   // <== this code helped me

                .gesture(
                    DragGesture()
                        .onChanged {
                            //
                        }
                        .onEnded {_ in
                            //
                        }
                )
        }
}

以上示例的代码可以如下所示:

struct ContentView: View {

@State var position1: CGPoint = CGPoint(x: 100, y: 150)
@State var lastPosition1: CGPoint = CGPoint(x: 100, y: 150)
let dragArea1: CGSize = CGSize(width: 200, height: 300)

@State var position2: CGPoint = CGPoint(x: 100, y: 150)
@State var lastPosition2: CGPoint = CGPoint(x: 100, y: 150)
let dragArea2: CGSize = CGSize(width: 200, height: 300)

var body: some View {
    
let drag = DragGesture()
    .onChanged {
        if $0.startLocation.x <= self.dragArea1.width {
            self.position1 = CGPoint(x: $0.translation.width + self.lastPosition1.x, y: $0.translation.height + self.lastPosition1.y)
        } else {
            self.position2 = CGPoint(x: $0.translation.width + self.lastPosition2.x, y: $0.translation.height + self.lastPosition2.y)
        }
    }
    .onEnded {_ in
        self.lastPosition1 = self.position1
        self.lastPosition2 = self.position2
    }
    
    return HStack {
        VStack {
            Rectangle().foregroundColor(.red)
                .frame(width: 150, height: 150)
                .position(self.position1)
                .clipped()
        }
        .background(Color.green)
        .frame(width: dragArea1.width, height: dragArea1.height)
        
        VStack {
            Rectangle().foregroundColor(.blue)
            .frame(width: 150, height: 150)
            .position(self.position2)
            .clipped()
        }
        .background(Color.yellow)
        .frame(width: dragArea2.width, height: dragArea2.height)
    }
    .clipShape(Rectangle())     //<=== This
    .contentShape(Rectangle())  //<=== and this
    .gesture(drag)
} }

你的回答不清楚,请重新格式化你的帖子。你的代码中有很多不必要的空格和未关闭的括号。 - GooDeeJAY
请重新格式化您的代码以获得完整的工作版本。您可以阅读指南答案应该是什么样子 - Lew Winczynski

0
对于第二部分(即更新原问题),这是我最终得到的内容。基本上,我将两个独立的拖动手势组合成一个手势,覆盖整个 HStack,并根据其在 HStack 中开始的位置将手势定向到适当的@State变量。
结果演示:

enter image description here

代码:

struct ContentView: View {
    
    @State var position1: CGPoint = CGPoint(x: 100, y: 150)
    @State var lastPosition1: CGPoint = CGPoint(x: 100, y: 150)
    let dragArea1: CGSize = CGSize(width: 200, height: 300)
    
    @State var position2: CGPoint = CGPoint(x: 100, y: 150)
    @State var lastPosition2: CGPoint = CGPoint(x: 100, y: 150)
    let dragArea2: CGSize = CGSize(width: 200, height: 300)
    
    var body: some View {

    let drag = DragGesture()
        .onChanged {
            guard $0.startLocation.y >= 0 && $0.startLocation.y <= self.dragArea1.height else { return }
            if $0.startLocation.x <= self.dragArea1.width {
                self.position1 = CGPoint(x: $0.translation.width + self.lastPosition1.x, y: $0.translation.height + self.lastPosition1.y)
            } else {
                self.position2 = CGPoint(x: $0.translation.width + self.lastPosition2.x, y: $0.translation.height + self.lastPosition2.y)
            }
        }
        .onEnded {_ in
            self.lastPosition1 = self.position1
            self.lastPosition2 = self.position2
        }
        
        return HStack {
            VStack {
                Rectangle().foregroundColor(.red)
                    .frame(width: 150, height: 150)
                    .position(self.position1)
                    .clipped()
            }
            .background(Color.green)
            .frame(width: dragArea1.width, height: dragArea1.height)
            
            VStack {
                Rectangle().foregroundColor(.blue)
                .frame(width: 150, height: 150)
                .position(self.position2)
                .clipped()
            }
            .background(Color.yellow)
            .frame(width: dragArea2.width, height: dragArea2.height)
        }
        .gesture(drag)
    }
}

注:

  1. 目前,手势在每个容器中都可以使用(即绿色和黄色区域),即使您不在红色或蓝色正方形内拖动也可以。
  2. 如果将整个手势代码放入视图并将其包装在GeometryReader中,以便您可以在“.onChanged”中上下文引用本地边界,则可能会更加灵活和/或具有更多控制。

虽然它能够工作,但是它的外观相当丑陋,因为你无法将每个组件内部的行为隔离开来。我需要一个这些组件的网格,而不是在一个父视图中管理它们。每个组件都应该包含自己的行为。 - Alexander Ulitin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接