10

So I've got the following code:

import SwiftUI

struct ContentView : View {
    @State private var draggingLocation = CGPoint.zero
    @State private var startLocation = CGPoint.zero
    @State private var dragging = false

    var body: some View {
        let GR = DragGesture(minimumDistance: 10, coordinateSpace: .global)
            .onEnded { value in
                self.dragging = false
                self.draggingLocation = CGPoint.zero
                self.startLocation = CGPoint.zero
            }
            .onChanged { value in
                if !self.dragging {
                    self.dragging = true
                }
                if self.startLocation == CGPoint.zero {
                    self.startLocation = value.startLocation
                }
                self.draggingLocation = value.location
            }

        return ZStack {
            if self.dragging {
                Path { path in
                    path.move(to: CGPoint(x: self.startLocation.x-5, y: self.startLocation.y-5))
                    path.addLine(to: CGPoint(x: self.draggingLocation.x-5, y: self.draggingLocation.y+5))
                    path.addLine(to: CGPoint(x: self.draggingLocation.x+5, y: self.draggingLocation.y-5))
                    path.addLine(to: CGPoint(x: self.startLocation.x+5, y: self.startLocation.y+5))
                }
                .fill(Color.black)
            }

            Circle()
            .fill(self.dragging ? Color.blue : Color.red)
            .frame(width: 100, height: 100)
            .gesture(GR)
            .offset(
                x: 75,
                y: 75
            )

            Circle()
            .fill(self.dragging ? Color.blue : Color.red)
            .frame(width: 100, height: 100)
            .gesture(GR)
            .offset(
                x: -75,
                y: -75
            )
        }
        .frame(width: 400, height: 400)
        .background(Color.gray)
    }
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

Which results in this behavior:

ios

I'd like to be able to drag the edge out from one circle and into the other, the problem of course is that the coordinate space of the Path is relative to the gray box (ContentView) and not global. A Path has a property coordinateSpace in the documentation but there's very little information how to use it, and googling the term with SwiftUI literally returns three results, all of which are really just links to Apple's currently sparse docs. Anyone have an idea on how to best approach this?

adammenges
  • 7,848
  • 5
  • 26
  • 33

1 Answers1

13

Coordinate spaces come in three flavours: .local, .global and .named. The first two are obvious. The third, named coordinate spaces, are extremely useful in cases like yours. They are also useful in combination with GeometryReader. For more details on that, check https://swiftui-lab.com/geometryreader-to-the-rescue/

Named coordinate spaces let you express a coordinate of one view, in the coordinate space of another. For that, SwiftUI let you specify a name for a view's coordinate space. Then, in other places of your code, you can make a reference of it. In your example, naming the coordinate of your ZStack is the way to go.

Here's the refactored code:

Note: I moved the Path below, only so that it draws in front of the circles. And also, pay attention to the first Path, which is only there to prevent what I think is a bug in ZStack.

enter image description here

import SwiftUI

struct ContentView : View {
    @State private var draggingLocation = CGPoint.zero
    @State private var startLocation = CGPoint.zero
    @State private var dragging = false

    var body: some View {
        let GR = DragGesture(minimumDistance: 10, coordinateSpace: .named("myCoordinateSpace"))
            .onEnded { value in
                self.dragging = false
                self.draggingLocation = CGPoint.zero
                self.startLocation = CGPoint.zero
        }
        .onChanged { value in
            if !self.dragging {
                self.dragging = true
            }

            if self.startLocation == CGPoint.zero {
                self.startLocation = value.startLocation
            }
            self.draggingLocation = value.location
        }

        return ZStack(alignment: .topLeading) {

            Circle()
                .fill(self.dragging ? Color.blue : Color.red)
                .frame(width: 100, height: 100)
                .overlay(Text("Circle 1"))
                .gesture(GR)
                .offset(x: 75, y: 75)

            Circle()
                .fill(self.dragging ? Color.blue : Color.red)
                .frame(width: 100, height: 100)
                .overlay(Text("Circle 2"))
                .gesture(GR)
                .offset(x: 200, y: 200)

            if self.dragging {
                Path { path in
                    path.move(to: CGPoint(x: self.startLocation.x-5, y: self.startLocation.y-5))
                    path.addLine(to: CGPoint(x: self.draggingLocation.x-5, y: self.draggingLocation.y+5))
                    path.addLine(to: CGPoint(x: self.draggingLocation.x+5, y: self.draggingLocation.y-5))
                    path.addLine(to: CGPoint(x: self.startLocation.x+5, y: self.startLocation.y+5))
                }
                .fill(Color.black)
            }

        }
        .coordinateSpace(name: "myCoordinateSpace")
        .frame(width: 400, height: 400, alignment: .topLeading)
        .background(Color.gray)
    }
}
kontiki
  • 37,663
  • 13
  • 111
  • 125
  • Thank you! As a side note, what’s the best way to programmatically tell if the edge was ‘dropped’ over one circle or the other? Maybe without comparing drawn locations because that seems messy to say the least. – adammenges Jul 13 '19 at 22:54
  • 1
    You're welcome. It seems SwiftUI has DragAndDrop support, but only for macOS (https://developer.apple.com/documentation/swiftui/dropoperation). I'm hopeful they will eventually extend it to iOS. Better sooner than later. – kontiki Jul 14 '19 at 05:31
  • 1
    I updated the code. I though there was a bug in ZStack, but it was doing the right thing. I missed a couple of alignment parameters. For a detail of the changes you can view the edit differences of the post. – kontiki Jul 14 '19 at 18:16
  • Bummer, yeah hopefully they add it to iOS soon. Extending this a bit if there need to be two named coordinateSpace's, it seems to be based off the circles size and in the top left, not quite what I was expecting: Gif: https://imgur.com/a/R0E0p5h -- Code: https://gist.github.com/adammenges/c83f357888377c2ad3fa6494090dddd0 – adammenges Jul 14 '19 at 20:19
  • 3
    In addition to .coordinateSpace(), you may profit from using anchor preferences (Anchor and Anchor). They are opaque types that hold geometry and coordinate space together. You use a GeometryProxy subscript function to "decode" them. It's an extensive topic, but I've written a whole article about it. Here's the link: https://swiftui-lab.com/communicating-with-the-view-tree-part-1/ – kontiki Jul 15 '19 at 14:23