I am reading the frame of a white-colored view in my macOS SwiftUI code. I want to place a circle at the origin of this white-colored view. I am using .global as the CoordinateSpace since both the white view and the red circle have access to the global coordinate space and are in the same hierarchy.
I should be able to successfully position the circle at the origin of the white view, but for some reason—I believe due to my app's title bar being 28.0 points high—the circle’s Y position is not aligning as expected.
How can I solve this issue? This doesn't make sense to me because we are dealing with the global CoordinateSpace, and for a given CGPoint in the global coordinate system, there should be only one exact position.
import SwiftUI
struct ContentView: View {
@State private var rect: CGRect = CGRect()
var body: some View {
ZStack {
Color.white
.frame(width: 300.0, height: 300.0)
.background(
GeometryReader { geometryValue in
let frame = geometryValue.frame(in: .global)
Color.clear
.onAppear {
print("onAppear global:", frame)
rect = frame
}
.onChange(of: frame) { newValue in
print("onChange global:", newValue)
rect = newValue
}
}
)
.padding(25.0)
.border(Color.black)
Circle()
.fill(Color.red)
.opacity(0.75)
.frame(width: 50.0, height: 50.0)
.position(x: rect.origin.x, y: rect.origin.y)
}
}
}
It seems like your misunderstanding is that you think .position
takes in a coordinate that is in the global coordinate space. This is not true.
.position
actually expands the logical frame of the view to fill all the available space, much like a Rectangle
or Color
. The x and y coordinates you pass to .position
is relative to this frame. Try adding a .border(.red)
after .position(...)
and see the bounds of the frame.
You can see this even more clearly with this code:
HStack {
Color.green
Circle()
.fill(Color.red)
.opacity(0.75)
.frame(width: 50, height: 50)
.position(x: 0, y: 0)
Color.blue
}
All three views in the HStack
are views that try to take up as much space as possible, and so HStack
will resize them to let them fill one third of the horizontal space each. The circle ends up being positioned at the top right of the green, instead of at the global (0, 0), which is the top left of the green.
Going back to your code, since the circle is in a ZStack
, .position
will expand to cover the whole ZStack
. The ZStack
does not extend beyond the title bar by default. On the other hand, the .global
coordinate space does include the title bar, as you have found out.
To position the red circle correctly, the coordinate space you should use is that of the ZStack
.
ZStack {
Color.white
.frame(width: 300.0, height: 300.0)
// on macOS 13+, you can use onGeometryChange which is much less boilerplate!
.onGeometryChange(for: CGRect.self) {
$0.frame(in: .named("X"))
} action: {
rect = $0
}
// for before macOS 13:
/*
.background {
GeometryReader { geometryValue in
let frame = geometryValue.frame(in: .named("X"))
Color.clear
.onAppear {
print("onAppear global:", frame)
rect = frame
}
.onChange(of: frame) { newValue in
print("onChange global:", newValue)
rect = newValue
}
}
}
*/
.padding(25)
.border(Color.black)
Circle()
.fill(Color.red)
.opacity(0.75)
.frame(width: 50, height: 50)
.position(x: rect.origin.x, y: rect.origin.y)
}
.coordinateSpace(.named("X")) // name the coordinate space of the ZStack!
// before macOS 14:
// .coordinateSpace(name: "X")
Alternatively, if you add .ignoresSafeArea()
to the ZStack
, then the ZStack
(and its coordinate space) will coincide with that of the global coordinate space (since the title bar counts as an "unsafe area"), and using the .global
coordinate space will position the circle correctly as well.