如何在 SwiftUI 中相对于不同坐标系中的其他视图定位视图

How to position views relative to other views in different coordinate systems in SwiftUI

我正在尝试使用 SwiftUI 在我的应用程序中实现拖放功能。

我创建了两个圆圈,它们位于两个不同的 HStacks。它们不共享相同的坐标 space.

带笔划的圆圈是目标,绿色实心圆圈是要拖动的对象。

我能够使用 .overlay 中的 GeometryReader 获得它们的绝对位置。当对象圆被拖动到目标圆上时,我用它来检测它们是否重叠。这行得通。

当它们不重叠时,对象圈将移回原来的位置。当它们确实重叠时,对象圈应该在目标圈的位置捕捉​​到位。这是我似乎有问题的地方。

我正在尝试通过以下方式设置对象圆的新 X 和 Y 位置: 物圈局部位置 - 物圈全局位置 + 目标圈全局位置。

    objectPosition = CGPoint(
        x: objectPosition.x - objectFrame.midX + targetFrame.midX,
        y: objectPosition.y - objectFrame.midY + targetFrame.midY
   )

我假设这会将我带到目标圈的坐标 space。但是不知何故它不起作用。

到目前为止,我一直无法找到一种可靠且简单的方法来在 SwiftUI 中转换坐标 spaces。在覆盖层内使用 GeometryReader 的解决方法至少给我正确的全局位置。但是我还没有找到一种方法来使用这些位置将视图也放置在 .global 坐标 space 中。

如果有人知道为什么我的坐标计算 space 是错误的,或者甚至知道一种更容易地相互转换和定位视图的方法,我将不胜感激。

这是我的 SwiftUI 单视图代码 iOS 应用程序:

import SwiftUI

struct ContentView: View {
    
    @State private var isDragging: Bool = false
    
    @State private var objectDragOffset: CGSize = .zero
    @State private var objectPosition: CGPoint = .zero
    
    @State private var objectFrame: CGRect = .zero
    @State private var targetFrame: CGRect = .zero
    
    var body: some View {
        VStack {
            HStack {
                Circle()
                    .stroke(lineWidth: 3)
                    .fill(Color.blue)
                    .frame(width: 100.0, height: 100.0)
                    .overlay(
                        GeometryReader { targetGeometry in
                            Color(.clear)
                                .onAppear { targetFrame = targetGeometry.frame(in: .global) }
                        }
                    )
                    .position(CGPoint(x:180, y: 190))
            }.background(Color.yellow)
            HStack {
                Circle()
                    .foregroundColor(.green)
                    .frame(width: 100, height: 100, alignment: .center)
                    .overlay(
                        GeometryReader { objectGeometry in
                            Color(.clear)
                                .onAppear {
                                    objectFrame = objectGeometry.frame(in: .global) }
                        }
                    )
                    .position(objectPosition)
                    .offset( isDragging ? objectDragOffset : .zero)
                    .onAppear { objectPosition = CGPoint(x: 200, y: 250 ) }
                    .gesture(
                        DragGesture(coordinateSpace: .global)
                            .onChanged { drag in
                                isDragging = true
                                objectDragOffset = drag.translation
                            }
                            .onEnded { drag in
                                isDragging = false
                                
                                if targetFrame.contains(drag.location) {
                                    objectPosition = CGPoint(
                                        x: objectPosition.x - objectFrame.midX + targetFrame.midX,
                                        y: objectPosition.y - objectFrame.midY + targetFrame.midY
                                    )
                                } else {
                                    objectPosition = CGPoint(x: 200, y: 250 )
                                }
                            }
                    )
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

经过多次尝试,我找到了一个有效的解决方案,无论创建了多少视图。 使用 GeometryReader 作为每个 Circle 的父对象,我得到了我想要拖动的对象的 .global.local 位置。我从其本地位置中减去全局位置,然后添加目标的全局位置。这给了我对象的新绝对位置,它的目的地,在它的局部坐标 space 中。 为了方便和将来使用,我的代码还实现了拖放功能以及 CircleViewModifier。 我正在使用 Circles 的两个底层 CGRects 来测试交集。 重要的是要注意圆的初始位置也使用 GeometryReader

设置

如果这可以简化,请post您的评论或回答。

import SwiftUI

struct ContentView: View {

    @State private var isDragging: Bool = false
    @State private var atTarget: Bool = false

    @State private var objectDragOffset: CGSize = .zero
    @State private var objectPosition: CGPoint = .zero
    @State private var objectGlobalPosition: CGPoint = .zero

    @State private var targetGlobalPosition: CGPoint = .zero
    @State private var newObjectPosition: CGPoint = .zero

    var body: some View {

        VStack {
            HStack {
                GeometryReader { geometry in
                    Circle()
                        .stroke(lineWidth: 3)
                        .fill(Color.blue)
                        .modifier(CircleModifier())
                        .position(CGPoint(x:geometry.frame(in: .local).midX, y: geometry.frame(in: .local).midY))
                        .onAppear() {
                            targetGlobalPosition = CGPoint(x:geometry.frame(in: .global).midX, y: geometry.frame(in: .global).midY)
                        }
                }
            }.background(Color.yellow)

            HStack {
                GeometryReader { geometry in
                    Circle()
                        .foregroundColor(.red)
                        .position(atTarget ? newObjectPosition : CGPoint(x:geometry.frame(in: .local).midX, y: geometry.frame(in: .local).midY))
                        .modifier(CircleModifier())
                        .onAppear() {
                            objectPosition = CGPoint(x:geometry.frame(in: .local).midX, y: geometry.frame(in: .local).midY)
                            objectGlobalPosition = CGPoint(x:geometry.frame(in: .global).midX, y: geometry.frame(in: .global).midY)
                        }
                        .offset(isDragging ? objectDragOffset : .zero)
                        .gesture(
                            DragGesture(coordinateSpace: .global)
                                .onChanged { drag in
                                    isDragging = true
                                    objectDragOffset = drag.translation

                                    newObjectPosition = CGPoint(
                                        x: objectPosition.x - objectGlobalPosition.x + targetGlobalPosition.x,
                                        y: objectPosition.y - objectGlobalPosition.y + targetGlobalPosition.y
                                    )
                                }
                                .onEnded { drag in
                                    isDragging = false

                                    let targetFrame = CGRect(origin: targetGlobalPosition, size: CircleModifier.frame)
                                    let objectFrame = CGRect(origin: objectGlobalPosition, size: CircleModifier.frame)
                                        .offsetBy(dx: drag.translation.width, dy: drag.translation.height)
                                        .insetBy(dx: CircleModifier.frame.width * 0.1, dy: CircleModifier.frame.height * 0.1)

                                    if targetFrame.intersects(objectFrame) {
                                        // Will check for the intersection of the rectangles, not the circles. See above insetBy adjustment to get a good middle ground.
                                        atTarget = true
                                    } else {
                                        atTarget = false
                                    }
                                }
                        )
                }
            }
        }
    }
}

struct CircleModifier: ViewModifier {
    static let frame = CGSize(width: 100.0, height: 100.0)

    func body(content: Content) -> some View {
        content
            .frame(width: CircleModifier.frame.width, height: CircleModifier.frame.height, alignment: .center)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

基于上述解决方案,我创建了一个使用内部 .offset 对齐视图的修饰符。也许有人觉得这有帮助:

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public extension View {
    /// Positions the center of this view at the specified point in the specified
    /// coordinate space using offset.
    ///
    /// Use the `openRelativeOffset(_ position:in:)` modifier to place the center of a view at a
    /// specific coordinate in the specified coordinate space using a
    /// CGPoint to specify the `x`
    /// and `y` position of the target CoordinateSpace defined by the Enum `coordinateSpace`
    /// This is not changing the position of the view by internally using an offset, other views using auto layout should not be affected.
    ///
    ///     Text("Position by passing a CGPoint() and CoordinateSpace")
    ///         .openRelativeOffset(CGPoint(x: 175, y: 100), in: .global)
    ///         .border(Color.gray)
    ///
    /// - Parameters
    ///   - position: The point in the target CoordinateSpace at which to place the center of this. Uses auto layout if nil.
    ///   view.
    ///   - in coordinateSpace: The target CoordinateSpace at which to place the center of this view.
    ///
    /// - Returns: A view that fixes the center of this view at `position` in `coordinateSpace` .
    func openRelativeOffset(_ position: CGPoint?, in coordinateSpace: CoordinateSpace) -> some View {
        modifier(OpenRelativeOffset(position: position, coordinateSpace: coordinateSpace))
    }
}

private struct OpenRelativeOffset: ViewModifier {

    var position: CGPoint?
    @State private var newPosition: CGPoint = .zero
    @State private var newOffset: CGSize = .zero

    let coordinateSpace: CoordinateSpace

    @State var localPosition: CGPoint = .zero
    @State var targetPosition: CGPoint = .zero

    func body(content: Content) -> some View {

        if let position = position {
            return AnyView(
                content
                    .offset(newOffset)
                    .background(
                        GeometryReader { geometry in
                            Color.clear
                                .onAppear {
                                    let localFrame = geometry.frame(in: .local)
                                    let otherFrame = geometry.frame(in: coordinateSpace)

                                    localPosition = CGPoint(x: localFrame.midX, y: localFrame.midY)

                                    targetPosition = CGPoint(x: otherFrame.midX, y: otherFrame.midY)
                                    newPosition.x = localPosition.x - targetPosition.x + position.x
                                    newPosition.y = localPosition.y - targetPosition.y + position.y

                                    newOffset = CGSize(width: newPosition.x - abs(localPosition.x), height: newPosition.y - abs(localPosition.y))
                                }
                        }
                    )
            )
        } else {
            return AnyView(
                content
            )
        }
    }
}

源代码也可在此存储库中使用,内部使用 .position.

的另一个版本

https://github.com/marcoboerner/OpenSwiftUIViews