在 SwiftUI 中,如何使手势在容器视图之外处于非活动状态?

Posted

技术标签:

【中文标题】在 SwiftUI 中,如何使手势在容器视图之外处于非活动状态?【英文标题】:In SwiftUI, how can you make a gesture inactive outside a container view? 【发布时间】:2020-11-12 21:27:30 【问题描述】:

我正在尝试使视图仅在其剪辑容器视图中可拖动和/或可缩放(否则它可能会遇到并与其他视图的手势冲突),但到目前为止我没有尝试过阻止手势扩展在容器的可见边界之外。

这是我想要的行为的简化演示......

当红色 Rectangle 部分超出绿色 VStack 区域(被剪裁)时,它会响应超出绿色区域的拖动手势:

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 对上述问题的解决方案效果很好,但是当我在上面的容器旁边添加第二个可拖动容器时,我在第一个容器中得到一个“死区”,我无法在其中拖动(它似乎是第二个正方形的位置如果它没有被剪裁,将覆盖第一个)。问题只发生在原始/左侧,而不是新的。我认为这与它具有更高的优先级有关,因为它被排在第二位。

这是新问题的插图:

这是更新后的代码:

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)
        
        
    

关于如何在任何容器外保持禁用拖动的任何想法,如已经实现的那样,但也允许在每个容器的完整范围内拖动,而不管其他容器发生什么?

【问题讨论】:

【参考方案1】:

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

使用 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!对于所发布的问题,这是一个非常优雅的解决方案,并且效果很好。我已经用一个后续问题更新了这个问题,该问题是由于将其中两个可拖动区域彼此相邻而产生的。如果您有解决此问题的好主意,请查看并告诉我。非常感谢! 好的,我想出了如何做这个的第二部分。我会将我的工作作为单独的答案发布。再次感谢@Asperi!【参考方案2】:

这两天我一直在寻找类似问题的解决方案 @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)
 

【讨论】:

你的答案不清楚,重新格式化你的帖子,你的代码中使用了很多不必要的空格和未闭合的括号 请重新格式化您的代码以获得完整的工作版本。您可以阅读guidelines 和how an answer 应该是什么样子。 非常感谢!这为我解决了问题。【参考方案3】:

对于第二部分(即更新到原始问题),这就是我最终得到的结果。基本上,我将两个单独的拖动手势组合成一个覆盖整个 HStack 的手势,然后根据它在 HStack 中的起始位置将手势定向到适当的 @State 变量。

结果演示:

代码:

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)
    

注意事项:

    就像现在一样,手势可以在每个容器中的任何位置(即绿色和黄色区域)工作,即使您没有在红色或蓝色方块内拖动也是如此。 如果您将整个手势代码放入视图中并将其包装在 GeometryReader 中,这样您就可以在“.onChanged”内的上下文中引用本地边界,这可能会更加通用和/或提供更多控制。李>

【讨论】:

它工作得很好,但它很丑,因为你不能隔离每个组件内部的行为。我需要这些组件的网格,我不想在一个超级视图中管理它们。他们每个人都应该包含自己的行为。

以上是关于在 SwiftUI 中,如何使手势在容器视图之外处于非活动状态?的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI手势(Gesture)进阶 : 实现任意视图的长按惯性加速行为

SwiftUI手势(Gesture)进阶 : 实现任意视图的长按惯性加速行为

如何使集合视图响应其自身视图之外的平移手势

如何使 SwiftUI 列表行背景颜色扩展整个宽度,包括安全区域之外

为啥在 SwiftUI 中点击按钮的手势甚至在它的框架之外也会被触发?

如何在 SwiftUI 中拥有 2 个手势?