I have a circle view that is not in the same hierarchy where I can access the needed coordinateSpace in another hierarchy. My app's UI does not allow me to place my circle view within the hierarchy of the coordinateSpace. Even if I did, there are two different coordinateSpace instances in two separate hierarchies. The issue is how to access and use the coordinateSpace from another hierarchy to set my circle's position at that origin. I am using macOS 12.
import SwiftUI
struct ContentView: View {
@State private var rectWhite: CGRect = CGRect()
@State private var rectBlue: CGRect = CGRect()
@State private var circlePoint: CGPoint = CGPoint()
var body: some View {
VStack {
HStack {
Color.white
.frame(width: 200.0, height: 200.0)
.background {
GeometryReader { geometryValue in
let frame = geometryValue.frame(in: .named("white"))
Color.clear
.onAppear {
print("onAppear white:", frame)
rectWhite = frame
}
.onChange(of: frame) { newValue in
print("onChange white:", newValue)
rectWhite = newValue
}
}
}
.coordinateSpace(name: "white")
.padding(25)
.border(Color.black)
Spacer()
Color.blue
.frame(width: 200.0, height: 200.0)
.background {
GeometryReader { geometryValue in
let frame = geometryValue.frame(in: .named("blue"))
Color.clear
.onAppear {
print("onAppear blue:", frame)
rectBlue = frame
}
.onChange(of: frame) { newValue in
print("onChange blue:", newValue)
rectBlue = newValue
}
}
}
.coordinateSpace(name: "blue")
.padding(25)
.border(Color.black)
}
Circle()
.fill(Color.red)
.opacity(0.75)
.frame(width: 50, height: 50)
.position(circlePoint)
HStack {
Button("set on white origin") {
circlePoint = rectWhite.origin
}
Button("set on blue origin") {
circlePoint = rectBlue.origin
}
}
}
.padding()
}
}
Views can only read the coordinate spaces of their parents. You should put .coordinateSpace(name:)
at the parent of all three views you are interested in, and work in that coordinate space only.
What complicates things is that the coordinate space that .position
is working in, is a bit weird. As I explained in this answer, .position
first takes up as much space as possible, and the CGPoint
you give it is treated as a point in the rectangle it ends up taking up.
I'd recommend restructuring the view so that the circle is in a ZStack
, so that .position
would use the same coordinate space as the ZStack
, as the circle will be the same size as the ZStack
.
struct ContentView: View {
// all these are in the parent coordinate space
@State private var rectWhite: CGRect = CGRect()
@State private var rectBlue: CGRect = CGRect()
@State private var circlePoint: CGPoint = CGPoint()
var body: some View {
ZStack {
VStack {
HStack {
Color.white
.frame(width: 200.0, height: 200.0)
.modifier(FrameReader(frame: $rectWhite, coordinateSpace: .named("parent")))
.padding(25)
.border(Color.black)
Spacer()
Color.blue
.frame(width: 200.0, height: 200.0)
.modifier(FrameReader(frame: $rectBlue, coordinateSpace: .named("parent")))
.padding(25)
.border(Color.black)
}
HStack {
Button("set on white origin") {
circlePoint = rectWhite.origin
}
Button("set on blue origin") {
circlePoint = rectBlue.origin
}
}
}
Circle()
.fill(Color.red)
.opacity(0.75)
.frame(width: 50, height: 50)
.position(circlePoint)
}
.coordinateSpace(name: "parent")
.padding()
}
}
If you want to keep the circle in a VStack
, you would also need to read the circle's frame relative to the parent's coordinate space, and do some conversions before passing it to .position
.
struct ContentView: View {
// all these are in the parent coordinate space
@State private var rectWhite: CGRect = CGRect()
@State private var rectBlue: CGRect = CGRect()
@State private var circleFrame: CGRect = CGRect() // New @State here
@State private var circlePoint: CGPoint = CGPoint()
var body: some View {
VStack {
HStack {
Color.white
.frame(width: 200.0, height: 200.0)
.modifier(FrameReader(frame: $rectWhite, coordinateSpace: .named("parent")))
.padding(25)
.border(Color.black)
Spacer()
Color.blue
.frame(width: 200.0, height: 200.0)
.modifier(FrameReader(frame: $rectBlue, coordinateSpace: .named("parent")))
.padding(25)
.border(Color.black)
}
Circle()
.fill(Color.red)
.opacity(0.75)
.frame(width: 50, height: 50)
// note this conversion we are doing
.position(x: circlePoint.x - circleFrame.origin.x, y: circlePoint.y - circleFrame.origin.y)
.modifier(FrameReader(frame: $circleFrame, coordinateSpace: .named("parent")))
HStack {
Button("set on white origin") {
circlePoint = rectWhite.origin
}
Button("set on blue origin") {
circlePoint = rectBlue.origin
}
}
}
.coordinateSpace(name: "parent")
.padding()
}
}
In both of the above code snippets, I have extracted the GeometryReader
into its own view modifier, presented below for completeness.
struct FrameReader: ViewModifier {
@Binding var frame: CGRect
let coordinateSpace: CoordinateSpace
func body(content: Content) -> some View {
content
.background {
GeometryReader { geometryValue in
let frame = geometryValue.frame(in: coordinateSpace)
Color.clear
.onAppear {
self.frame = frame
}
.onChange(of: frame) { newValue in
self.frame = newValue
}
}
}
}
}