TransWikia.com

In SwiftUI, how can you make a gesture inactive outside a container view?

Stack Overflow Asked by rliebert on December 16, 2021

I’m trying to make a view draggable and/or zoomable only within its clipping container view (otherwise it can run into and conflict with other views’ gestures), but nothing I’ve tried so far keeps the gesture from extending outside the visible boundary of the container.

Here’s a simplified demo of the behavior I don’t want…

When the red Rectangle goes partially outside the green VStack area (clipped), it responds to drag gestures beyond the green area:
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())

How would you limit this gesture to only work inside the container (green area in the example above)?

UPDATE:
@Asperi’s solution to the above works well, but when I add a second draggable container next to the one above, I get a "dead area" in the first container inside which I can’t drag (it appears to be where the second square would cover the first one if it were not clipped). The problem only happens to the original/left side, not to the new one. I think that has to do with it having higher priority since it is drawn second.

Here’s an illustration of the new issue:

2 Containers create dragging dead zone

And here’s the updated code:

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

Any ideas of how to keep dragging disabled outside any containers, as already achieved, but also allow dragging within the full bounds of each container regardless of what happens with others?

3 Answers

Two days I was looking for a solution to a similar problem @Asperi's solution helps, but it is not universal for 3 or more figures

My solution: I adding

.contentShape(Rectangle())

before

.gesture(DragGesture().onChanged {

this article helped me. https://www.hackingwithswift.com/quick-start/swiftui/how-to-control-the-tappable-area-of-a-view-using-contentshape

I hope it will be useful to someone.

Sample code:

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

For the example above, the code could be like this:

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

Answered by John on December 16, 2021

For the 2nd part (i.e. update to the original question), here's what I ended up with. Basically, I combined the two separate drag gestures into one gesture that covers the whole HStack, and then directed the gesture to the appropriate @State variable depending on where in the HStack it started.

Demo of the Result:

enter image description here

Code:

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

Notes:

  1. As it is now, the gesture works anywhere in each container (i.e. green and yellow areas), even if you don't drag inside the red or blue square.
  2. This could probably be more versatile and/or give a bit more control if you put the whole gesture code into the view and wrapped it in a GeometryReader so you could reference the local bounds in context inside ".onChanged".

Answered by rliebert on December 16, 2021

Here is possible solution. The idea is to have drag coordinates in container coordinate space and ignore drag if start location is out of that named area.

Tested with Xcode 11.4 / iOS 13.4

demo

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

    }
}

Answered by Asperi on December 16, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP