0

The following code shows an orange screen with a green circle in the lower right. The circle can be dragged.

import SwiftUI

struct DraggableCircle: View {
    @Binding var offset: CGSize
    @State private var previousOffset: CGSize

    var body: some View {
        Circle().fill(Color.green)
            .frame(width: 100)
            .offset(self.offset)
            .gesture(
                DragGesture(minimumDistance: 0, coordinateSpace: .local)
                    .onChanged { event in
                        print("\nDragGesture onChanged")
                        self.offset = CGSize(width: event.location.x - (event.startLocation.x - self.previousOffset.width),
                                             height: event.location.y - (event.startLocation.y - self.previousOffset.height))
                    }
            )
    }

    init(offset: Binding<CGSize>) {
        print("Init with offset \(offset.wrappedValue)")
        self._offset = offset
        self._previousOffset = State(initialValue: offset.wrappedValue)
        print("offset = \(self.offset), previousOffset=\(self.previousOffset)")
    }
}

struct ContentView: View {
    @State var circleOffset = CGSize()

    var body: some View {
        GeometryReader { reader in
            Rectangle().fill(Color.orange)
                .overlay(
                    DraggableCircle(offset: self.$circleOffset)
                )
                .onAppear {
                    self.circleOffset = CGSize(width: reader.size.width / 2,
                                               height: reader.size.height / 2)
                    print("size: \(reader)\n")
                }
        }
    }
}

If you run and tap the green circle (to begin a drag gesture), the following appears in the console:

Init with offset (0.0, 0.0)
offset = (0.0, 0.0), previousOffset=(0.0, 0.0)
size: GeometryProxy(base: SwiftUI._PositionAwareLayoutContext(base: SwiftUI.LayoutTraitsContext(context: AttributeGraph.AttributeContext<SwiftUI.VoidAttribute>(graph: __C.AGGraphRef(p: 0x00007f84b6a05ff0), attribute: AttributeGraph.Attribute<()>(identifier: __C.AGAttribute(id: 42))), environmentIndex: 4), dimensionsIndex: 1, transformIndex: 3, positionIndex: 2), seed: 1, viewGraph: Optional(SwiftUI.ViewGraph))

Init with offset (187.5, 323.5)
offset = (187.5, 323.5), previousOffset=(187.5, 323.5)

DragGesture onChanged
Init with offset (0.0, 0.0)
offset = (0.0, 0.0), previousOffset=(0.0, 0.0)

What I expected to happen, is that when you drag the circle, it can be smoothly dragged somewhere else on the screen.

What actually happens, is when dragging starts, DraggableCircle.init is called again, which resets the offset and places the circle right in the middle. Why is this?

Note: when you move the @State previousOffset into the ContentView, the issue disappears. But I don't understand why.

Bart van Kuik
  • 4,704
  • 1
  • 33
  • 57

1 Answers1

0

I changed my code based on your comment. In my code this drag gesture workes fine. The only bug right now I see is in the first time drag gesture is activated. Maybe this will lead you to your desired solution.

        struct DraggableCircle: View {
    @Binding var circleOffset: CGSize
    @State private var previousOffset: CGSize

    var body: some View {
        Circle().fill(Color.green)
            .frame(width: 100)
            .offset(self.circleOffset)
            .gesture(
                DragGesture()
                    .onChanged { event in
                        print("\nDragGesture onChanged")
                            self.circleOffset = CGSize(width: event.location.x - (event.startLocation.x - self.previousOffset.width),
                                                       height: event.location.y - (event.startLocation.y - self.previousOffset.height))
                }
                .onEnded { event in
                    self.circleOffset = CGSize(width: event.location.x - (event.startLocation.x - self.previousOffset.width),
                                               height: event.location.y - (event.startLocation.y - self.previousOffset.height))

                    self.previousOffset = self.circleOffset
                }
        )
    }

    init(offset: Binding<CGSize>) {
        print("Init with offset \(offset.wrappedValue)")
        self._circleOffset = offset
        self._previousOffset = State(initialValue: offset.wrappedValue)
        print("offset = \(self.offset), previousOffset=\(self.previousOffset)")
    }
}

struct ContentView: View {
    @State var circleOffset = CGSize()

    var body: some View {
        GeometryReader { reader in
            Rectangle().fill(Color.orange)
                .overlay(
                    DraggableCircle(offset: self.$circleOffset)
                )
                .onAppear {
                    self.circleOffset = CGSize(width: reader.size.width / 2,
                                               height: reader.size.height / 2)
                    print("size: \(reader)\n")
                }
        }
    }
}
user832
  • 796
  • 5
  • 18
  • Unfortunately it's not possible to remove the State variable from the ContentView. The reason is, that in my original code (not the compressed example I posted), other views depend on it. – Bart van Kuik Jun 01 '20 at 06:08
  • @BartvanKuik I have changed the code, but that isn't perfect solution for now. – user832 Jun 01 '20 at 06:58
  • I don't see any difference between your current code and my code? I think something went wrong copy/pasting :) – Bart van Kuik Jun 01 '20 at 18:17
  • @BartvanKuik Ahh no copy pasting was perfectly fine.. i have added a `onEnded` modifier to Drag Gesture. It solves the problem of drag places the circle right in the middle. It just happens for the first time. – user832 Jun 01 '20 at 21:46