如何在 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 中。
为了方便和将来使用,我的代码还实现了拖放功能以及 Circle
的 ViewModifier
。
我正在使用 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
.
的另一个版本
我正在尝试使用 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 中。
为了方便和将来使用,我的代码还实现了拖放功能以及 Circle
的 ViewModifier
。
我正在使用 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
.