How to make some point make draggable trough set of views edges like on the video:
Instead of free drag:
I don't even know where to start since I can't measure the bounds of a view without GeometryReader.
However, GeometryReader isn't suitable in this case because these are different views on separate layers.
Sample View:
struct ContentView: View {
@State var point: CGPoint = .zero
var body: some View {
ZStack {
//foreach Nodes
NodeView()
NodeView()
.offset(x:0, y:100)
//foreach Points
BezierPoint(p1: $point)
}
}
}
struct NodeView : View {
// var nodeViewModel: NodeViewModel
// with exact location in space
var body: some View {
Text("Business")
.multilineTextAlignment(.center)
.foregroundStyle(.red)
.shadow(color: .black, radius: 2 )
.frame(minHeight: 40)
.padding( EdgeInsets(horizontal: 20, vertical: 14) )
.background {
// ANY Shape can be here
RoundedRectangle(cornerRadius: 10)
}
}
}
struct BezierPoint: View {
@Binding var p1: CGPoint
let pointsSize: CGFloat = 15
var body: some View {
GeometryReader { reader in
ControlPointHandle(size: pointsSize)
.offset( CGSize(width: p1.x + reader.size.width/2, height: p1.y + reader.size.height/2) )
.gesture(
DragGesture()
.onChanged { value in
self.p1 = value.location.relativeToCenter(of: reader.size, minus: true)
}
)
}
}
}
private struct ControlPointHandle: View {
let size: CGFloat
var body: some View {
Circle()
.frame(width: size, height: size)
.overlay(
Circle()
.stroke(Color.blue, lineWidth: 2)
)
.offset(x: -size/2, y: -size/2)
}
}
fileprivate extension CGPoint {
func relativeToCenter(of size: CGSize, minus: Bool = false) -> CGPoint {
let a: CGFloat = minus ? -1 : 1
return CGPoint(x: x + a * size.width/2, y: y + a * size.height/2)
}
}
How to make some point make draggable trough set of views edges like on the video:
Instead of free drag:
I don't even know where to start since I can't measure the bounds of a view without GeometryReader.
However, GeometryReader isn't suitable in this case because these are different views on separate layers.
Sample View:
struct ContentView: View {
@State var point: CGPoint = .zero
var body: some View {
ZStack {
//foreach Nodes
NodeView()
NodeView()
.offset(x:0, y:100)
//foreach Points
BezierPoint(p1: $point)
}
}
}
struct NodeView : View {
// var nodeViewModel: NodeViewModel
// with exact location in space
var body: some View {
Text("Business")
.multilineTextAlignment(.center)
.foregroundStyle(.red)
.shadow(color: .black, radius: 2 )
.frame(minHeight: 40)
.padding( EdgeInsets(horizontal: 20, vertical: 14) )
.background {
// ANY Shape can be here
RoundedRectangle(cornerRadius: 10)
}
}
}
struct BezierPoint: View {
@Binding var p1: CGPoint
let pointsSize: CGFloat = 15
var body: some View {
GeometryReader { reader in
ControlPointHandle(size: pointsSize)
.offset( CGSize(width: p1.x + reader.size.width/2, height: p1.y + reader.size.height/2) )
.gesture(
DragGesture()
.onChanged { value in
self.p1 = value.location.relativeToCenter(of: reader.size, minus: true)
}
)
}
}
}
private struct ControlPointHandle: View {
let size: CGFloat
var body: some View {
Circle()
.frame(width: size, height: size)
.overlay(
Circle()
.stroke(Color.blue, lineWidth: 2)
)
.offset(x: -size/2, y: -size/2)
}
}
fileprivate extension CGPoint {
func relativeToCenter(of size: CGSize, minus: Bool = false) -> CGPoint {
let a: CGFloat = minus ? -1 : 1
return CGPoint(x: x + a * size.width/2, y: y + a * size.height/2)
}
}
Share
Improve this question
edited Mar 3 at 1:04
Andrew
asked Mar 3 at 0:51
AndrewAndrew
11.6k9 gold badges84 silver badges118 bronze badges
0
1 Answer
Reset to default 3 +100The technique shown in the answer to Is it possible to detect which View currently falls under the location of a DragGesture? can be used to detect when a shape is under the drag point (it was my answer). This uses a GeometryReader
in the background of the shape, which should work even if you have a multi-layer view.
To find the point along the edge of the shape which is closest to the drag point, I would suggest the following approach:
- Determine whether the drag point is near the shape using the point-in-frame technique described in the other answer.
- If the drag point is near the shape, create two paths:
- A path representing the outline of the shape.
- A path consisting of a line that goes from the middle of the shape, through the drag point and then beyond.
- Use the
Path
functionlineIntersection(_:eoFill:)
to find the intersection of the line with the shape. - The last point in the intersection will be a point along the edge of the shape.
You were previously wrapping each point with a GeometryReader
. A GeometryReader
is greedy and consumes all the space available, so this was bloating the size of each point to the full size of the parent view. Instead of doing it that way, I would suggest using .onGeometryChange
to measure the position of each point.
Here is the updated example to show it working. It includes a second point, so that the independence of the points can be tested too.
import SwiftUI
struct ContentView: View {
@State private var dragLocation: CGPoint?
@State private var contactPoint: CGPoint?
@State private var nearestNodeId: Int?
@State private var previousNodeId: Int?
var body: some View {
ZStack {
ForEach(1...5, id: \.self) { i in
NodeView()
.background {
SplineContactDetector(nodeId: i,
shape: .rect(cornerRadius: 10),
dragLocation: $dragLocation,
contactPoint: $contactPoint,
nearestNodeId: $nearestNodeId,
previousNodeId: $previousNodeId
)
}
.offset(x:0, y: CGFloat(i) * 100 - 400)
}
//foreach Points
BezierPoint(dragLocation: $dragLocation, contactPoint: contactPoint)
BezierPoint(dragLocation: $dragLocation, contactPoint: contactPoint)
.offset(x:0, y:100)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(red: 0.99, green: 0.94, blue: 0.76))
.onChange(of: dragLocation) { _, newVal in
if newVal == nil {
contactPoint = nil
nearestNodeId = nil
previousNodeId = nil
}
}
}
}
/// ///////////////////
/// Basic Views
/// /////////////////
struct BezierPoint: View {
@Binding var dragLocation: CGPoint?
let contactPoint: CGPoint?
@State private var dragOffset: CGSize?
@State private var currentOffset = CGSize.zero
@State private var defaultFrame: CGRect?
let pointsSize: CGFloat = 15
private var offsetForContactPoint: CGSize? {
if let contactPoint, let defaultFrame {
CGSize(
width: contactPoint.x - defaultFrame.midX,
height: contactPoint.y - defaultFrame.midY
)
} else {
nil
}
}
private var offset: CGSize {
let result: CGSize
if let dragOffset {
if let offsetForContactPoint {
result = offsetForContactPoint
} else {
result = CGSize(
width: currentOffset.width + dragOffset.width,
height: currentOffset.height + dragOffset.height
)
}
} else {
result = currentOffset
}
return result
}
var body: some View {
Circle()
.fill(.blue)
.stroke(.primary, lineWidth: 2)
.frame(width: pointsSize, height: pointsSize)
.offset(offset)
.gesture(
DragGesture(minimumDistance: 1, coordinateSpace: .global)
.onChanged { value in
dragOffset = value.translation
dragLocation = value.location
}
.onEnded { value in
if let offsetForContactPoint {
currentOffset = offsetForContactPoint
}
dragOffset = nil
dragLocation = nil
}
)
.onGeometryChange(for: CGRect.self) { proxy in
proxy.frame(in: .global)
} action: { frame in
defaultFrame = frame
}
}
}
struct NodeView : View {
var body: some View {
Text("Business")
.multilineTextAlignment(.center)
.foregroundStyle(.red)
.shadow(color: .black, radius: 2 )
.frame(minHeight: 40)
.padding( EdgeInsets(horizontal: 20, vertical: 14) )
.background {
// ANY Shape can be here
RoundedRectangle(cornerRadius: 10)
}
}
}
/// ///////////////////
/// Helpers
/// /////////////////
struct SplineContactDetector<S: Shape> : View {
let nodeId: Int
let shape: S
@Binding var dragLocation: CGPoint?
@Binding var contactPoint: CGPoint?
@Binding var nearestNodeId: Int?
@Binding var previousNodeId: Int?
private let proximityMargin: CGFloat = 10
var body: some View {
GeometryReader { proxy in
let frame = proxy.frame(in: .global)
let proximity = proximity(nodeId: nodeId, frame: frame, shape: shape)
Color.clear
.onChange(of: proximity) { _, newVal in
if newVal.isNearby {
if nearestNodeId != nodeId {
nearestNodeId = nodeId
}
} else if nearestNodeId == nodeId {
previousNodeId = nodeId
nearestNodeId = nil
}
if let nearestPoint = newVal.nearestPoint {
contactPoint = nearestPoint
}
}
}
}
private func proximity(nodeId: Int, frame: CGRect, shape: S) -> ProximityInfo {
let result: ProximityInfo
if let dragLocation {
let isNearby = frame
.insetBy(dx: -proximityMargin, dy: -proximityMargin)
.contains(dragLocation)
if isNearby || (nearestNodeId == nil && previousNodeId == nodeId) {
let shapePath = shape.path(in: frame)
let joiningLine = Path { path in
path.move(to: CGPoint(x: frame.midX, y: frame.midY))
let dx = dragLocation.x - frame.midX
let dy = dragLocation.y - frame.midY
path.addLine(to: CGPoint(x: dx * 1000, y: dy * 1000))
}
let intersection = joiningLine.lineIntersection(shapePath)
result = ProximityInfo(isNearby: isNearby, nearestPoint: intersection.currentPoint)
} else {
result = ProximityInfo(isNearby: false, nearestPoint: nil)
}
} else {
result = ProximityInfo(isNearby: false, nearestPoint: nil)
}
return result
}
private struct ProximityInfo: Equatable {
let isNearby: Bool
let nearestPoint: CGPoint?
}
}
发布者:admin,转转请注明出处:http://www.yc00.com/questions/1745113363a4611978.html
评论列表(0条)