I defined a custom coordinate space at the parent level using a GeometryReader. Then, I tried to use that coordinate space in my child views, called RectangleView. However, it seems the children cannot access the coordinate space I defined. Instead, they fall back to the global coordinate system.
My goal is for the custom coordinate system to be independent and have its own freedom of coordinates. To test this, I defined a drag gesture for the GeometryReader, representing the custom coordinate space. My theory was that if I drag the GeometryReader, the origin of the custom coordinate space should remain at zero, and the frame or CGRect of the GeometryReader should remain unchanged. I also expected the coordinate system of the children to stay unaffected by the drag gesture.
However, in the actual test, I observed that the GeometryReader's frame and its children’s frames were affected by the drag. This is not what I expected. I wanted the GeometryReader and its children to maintain their original coordinate space values, regardless of the drag gesture of the parent view(But I want their origin to change if I drag them individually using their own drag gesture).
What am I doing wrong? Why are the coordinate values of the GeometryReader and its children changing with the drag gesture of parent GeometryReader?
Also, why can’t the children see the custom coordinate space even without the drag?
import SwiftUI
struct ContentView: View {
@State private var array: [CustomCGRectType] = [CustomCGRectType]()
private let coordinateSpaceName: String = "GeometryReader"
@State private var screenCGRect: CGRect = CGRect()
@State private var currentOffset: CGSize = .zero
@State private var lastOffset: CGSize = .zero
var body: some View {
VStack(spacing: 10.0) {
Text("GeometryReader: " + CustomCGRectType(id: coordinateSpaceName, rect: screenCGRect).description).font(.title3.monospaced())
GeometryReader { geometryValue in
ZStack {
Color.white.opacity(0.5).border(Color.black)
VStack {
RectangleView(id: "Blue Rectangle", color: Color.blue, size: CGSize(width: 50.0, height: 100.0), coordinateSpace: .named(coordinateSpaceName))
Spacer()
RectangleView(id: "Red Rectangle", color: Color.red, size: CGSize(width: 150.0, height: 50.0), coordinateSpace: .named(coordinateSpaceName))
}
HStack {
RectangleView(id: "Green Rectangle", color: Color.green, size: CGSize(width: 100.0, height: 100.0), coordinateSpace: .named(coordinateSpaceName))
Spacer()
RectangleView(id: "Yellow Rectangle", color: Color.yellow, size: CGSize(width: 50.0, height: 100.0), coordinateSpace: .named(coordinateSpaceName))
}
}
.onAppear {
screenCGRect = geometryValue.frame(in: .named(coordinateSpaceName))
}
.onChange(of: geometryValue.frame(in: .named(coordinateSpaceName))) { newValue in
screenCGRect = newValue
}
.onPreferenceChange(CustomCGRectPreferenceKey.self) { newValue in
array = newValue
}
}
.coordinateSpace(name: coordinateSpaceName)
.offset(currentOffset)
.gesture(dragGesture)
HStack(spacing: .zero) {
VStack(alignment: .trailing) {
ForEach(array) { item in
Text(item.id + ": ").font(.footnote.bold().monospaced())
}
}
VStack(alignment: .leading) {
ForEach(array) { item in
Text(item.description).font(.footnote.monospaced())
}
}
}
}
.padding()
}
private var dragGesture: some Gesture {
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged { gestureValue in
currentOffset = CGSize(
width: gestureValue.translation.width + lastOffset.width,
height: gestureValue.translation.height + lastOffset.height
)
}
.onEnded { gestureValue in
lastOffset = currentOffset
}
}
}
struct RectangleView: View {
let id: String
let color: Color
let size: CGSize
let coordinateSpace: CoordinateSpace
@State private var currentOffset: CGSize = .zero
@State private var lastOffset: CGSize = .zero
var body: some View {
Rectangle()
.fill(color.opacity(0.75))
.frame(width: size.width, height: size.height)
.captureFrame(with: id, in: .named(coordinateSpace), using: CustomCGRectPreferenceKey.self)
.offset(currentOffset)
.gesture(dragGesture)
}
private var dragGesture: some Gesture {
DragGesture(minimumDistance: 0, coordinateSpace: .named(coordinateSpace))
.onChanged { gestureValue in
currentOffset = CGSize(
width: gestureValue.translation.width + lastOffset.width,
height: gestureValue.translation.height + lastOffset.height
)
}
.onEnded { gestureValue in
lastOffset = currentOffset
}
}
}
struct CustomCGRectType: Identifiable, Equatable, CustomStringConvertible {
init(id: String, rect: CGRect) {
self.id = id
self.rect = rect
}
init() {
self.id = String()
self.rect = CGRect()
}
let id: String
var rect: CGRect
static func ==(lhs: Self, rhs: Self) -> Bool {
return (lhs.id == rhs.id) && (lhs.rect == rhs.rect)
}
var description: String {
return "(" + String(format: "%.2f", self.rect.origin.x) + ", " + String(format: "%.2f", self.rect.origin.y) + ", " + String(format: "%.2f", self.rect.size.width) + ", " + String(format: "%.2f", self.rect.size.height) + ")"
}
}
struct CustomCGRectPreferenceKey: PreferenceKey {
static var defaultValue: [CustomCGRectType] = [CustomCGRectType]()
static func reduce(value: inout [CustomCGRectType], nextValue: () -> [CustomCGRectType]) {
value.append(contentsOf: nextValue())
}
}
extension View {
func captureFrame(with id: String, in coordinateSpace: CoordinateSpace, using key: CustomCGRectPreferenceKey.Type) -> some View {
self.background(
GeometryReader { geometryValue in
Color.clear.preference(key: key, value: [CustomCGRectType(id: id, rect: geometryValue.frame(in: coordinateSpace))])
}
)
}
}
The Answer works but sometime I have this issue of -0.00, I am trying to fix this:
This is a typo that could be rather hard to spot. You wrote
.captureFrame(with: id, in: .named(coordinateSpace), using: CustomCGRectPreferenceKey.self)
But it should be
.captureFrame(with: id, in: coordinateSpace, using: CustomCGRectPreferenceKey.self)
.named(coordinateSpace)
is referring to a different (and non-existent) coordinate space with the name of coordinateSpace
. The coordinate space you want to refer to has the String
"GeometryReader" as its name, not another CoordinateSpace
.
You made the same typo in the DragGesture
, but it doesn't really matter there because you only use gestureValue.translation
, which doesn't depend on where the origin is.
After this change, you can still see the frames of the rectangles jumping around, but it will always be roughly the same value. This is because the screen only has a limited scale, but the DragGesture
is a lot more accurate.
Two views with offset(x: 0.00001)
and offset(x: 0)
will be placed at exactly the same place, for example. But frame(in:)
will take that 0.00001 into account if it sees that a coordinate space has been offsetted.
To fix this, you can "snap" the offsets according to the screen's scale. For example, if the screen scale is 1, that means the offset should be a whole number. If the screen scale is 2, the offset should be a whole number multiple of 0.5, and so on.
// change the offset to:
.offset(snapOffset(currentOffset))
// where snapOffset is:
@Environment(\.displayScale) var scale
func snapOffset(_ offset: CGSize) -> CGSize {
var snapped = offset
snapped.width = floor(offset.width * scale) / scale
snapped.height = floor(offset.height * scale) / scale
return snapped
}