Reputation: 13
I'm working on a macOS app with SwiftUI that should be able to create, arrange and delete geometric shapes on screen. The creation and dragging of shapes already works pretty well using a context menu.
import SwiftUI
class Canvas: ObservableObject {
@Published var nodes: [Node] = []
func addNode(position: CGPoint) -> Void {
nodes.append(Node(id: UUID(), position: position))
}
}
struct CanvasView: View {
@ObservedObject var canvas = Canvas()
var body: some View {
ZStack {
Color(red: 0.9, green: 0.9, blue: 0.8)
.contextMenu {
Button( action: {
self.canvas.addNode(position: CGPoint(x: 400, y: 400))
} )
{ Text("Add Node ...") }
}
ForEach(canvas.nodes) {node in
NodeView(node: node)
}
}
}
}
class Node: Identifiable, ObservableObject {
@Published var id: UUID
@Published var position: CGPoint
@Published var positionProxy: CGPoint
init (id: UUID, position: CGPoint) {
self.id = id
self.position = position
self.positionProxy = position
}
}
struct NodeView: View {
@ObservedObject var node: Node
init(node: Node) {
self.node = node
}
var draggingNode: some Gesture {
DragGesture(coordinateSpace: .global)
.onChanged { value in
self.node.position.x = value.translation.width + self.node.positionProxy.x;
self.node.position.y = -value.translation.height + self.node.positionProxy.y
}
.onEnded { value in
self.node.position.x = value.translation.width + self.node.positionProxy.x;
self.node.position.y = -value.translation.height + self.node.positionProxy.y;
self.node.positionProxy = self.node.position
}
}
var body: some View {
RoundedRectangle(cornerRadius: 20)
.fill(Color.white)
.frame(width: 100, height: 100)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(lineWidth: 1)
.fill(Color.gray)
)
.position(node.position)
.gesture(draggingNode)
}
}
My problem is that all created shapes appear at the same predefined location
position: CGPoint(x: 400, y: 400)
on screen and I have to move each of them manually to its intended position. I'm looking for a way to track the cursor position during right click or the context menu position to use it as node position and be able to write something like
self.canvas.addNode(position: cursorPosition)
instead of
self.canvas.addNode(position: CGPoint(x: 400, y: 400))
Is there any functionality in Swift, preferably in SwiftUI that solves my issue?
Upvotes: 0
Views: 350
Reputation: 6436
It is currently not possible to detect mouse click location in SwiftUI. A workaround for your issue may be to use a composition of SwiftUI Gesture
's.
Here is code that you can use to detect a (simple or double) left-click location:
import SwiftUI
struct ClickGesture: Gesture {
let count: Int
let coordinateSpace: CoordinateSpace
typealias Value = SimultaneousGesture<TapGesture, DragGesture>.Value
init(count: Int = 1, coordinateSpace: CoordinateSpace = .local) {
precondition(count > 0, "Count must be greater than or equal to 1.")
self.count = count
self.coordinateSpace = coordinateSpace
}
var body: SimultaneousGesture<TapGesture, DragGesture> {
TapGesture(count: count)
.simultaneously(with: DragGesture(minimumDistance: 0, coordinateSpace: coordinateSpace))
}
func onEnded(perform action: @escaping (CGPoint) -> Void) -> some Gesture {
ClickGesture(count: count, coordinateSpace: coordinateSpace)
.onEnded { (value: Value) -> Void in
guard value.first != nil else { return }
guard let startLocation = value.second?.startLocation else { return }
guard let endLocation = value.second?.location else { return }
guard ((startLocation.x-1)...(startLocation.x+1)).contains(endLocation.x),
((startLocation.y-1)...(startLocation.y+1)).contains(endLocation.y) else { return }
action(startLocation)
}
}
}
extension View {
func onClickGesture(
count: Int,
coordinateSpace: CoordinateSpace = .local,
perform action: @escaping (CGPoint) -> Void
) -> some View {
gesture(ClickGesture(count: count, coordinateSpace: coordinateSpace)
.onEnded(perform: action)
)
}
func onClickGesture(
count: Int,
perform action: @escaping (CGPoint) -> Void
) -> some View {
onClickGesture(count: count, coordinateSpace: .local, perform: action)
}
func onClickGesture(
perform action: @escaping (CGPoint) -> Void
) -> some View {
onClickGesture(count: 1, coordinateSpace: .local, perform: action)
}
}
You can use it in a very similar fashion as onTapGesture()
or TapGesture
:
struct MyView: View {
var body: some View {
Rectangle()
.frame(width: 600, height: 400)
.onClickGesture(count: 2) { location in
print("Double tap at location \(location)")
}
}
}
You can additionally specify a CoordinateSpace.
Upvotes: 0