I am using GeometryReader
in SwiftUI to track the frame of a Color.black
background. However, when I resize the window, the size retrieved from GeometryReader
(via geo.frame(in: .local)
) unexpectedly shrinks, even though the Color.black
background visually remains the correct size.
Here’s the code I am using:
struct ContentView: View {
@State private var rect: CGRect = CGRect()
var body: some View {
GeometryReader { geo in
ZStack {
Color.black
.onAppear {
rect = geo.frame(in: .local)
}
.onChange(of: geo.frame(in: .local)) { oldValue, newValue in
rect = newValue
}
Color.red.frame(width: rect.width, height: 2.0)
Color.blue.frame(width: 2.0, height: rect.height)
Color.green
.frame(width: 300.0, height: 50.0)
.offset(y: -100.0)
}
}
.padding(300.0)
}
}
Why is the size value from the GeometryReader shrinking during a window resize, and how can I ensure it always matches the size of the Color.black background?
Mangak is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
This is because the size of the GeometryReader
is more flexible than the size of the Color.black
. It can change to a size that is smaller than the Color.black
. The Color.black
is always as large as the ZStack
, which is at least as large as the Color.green
.
You can see the size of the GeometryReader
by adding a .border(.red)
to the it. before the padding
.
So one way to solve this is just not to consider the cases where the size of the GeometryReader
is smaller than the Color.black
. It’s not very clear what should the view look like when that happens, anyway. You can do this by setting a minimum size:
GeometryReader { geo in
// ...
}
.frame(minWidth: 300, minHeight: 50)
.padding(300.0)
If you want the Color.black
to be resizable to a size smaller than the Color.green
, do not put them in the same ZStack
. Use an overlay
/background
to put one on top of/below the other.
GeometryReader { geo in
Color.black
.onAppear {
rect = geo.frame(in: .local)
}
.onChange(of: geo.frame(in: .local)) { oldValue, newValue in
rect = newValue
}
.overlay {
ZStack {
Color.red.frame(width: rect.width, height: 2.0)
Color.blue.frame(width: 2.0, height: rect.height)
Color.green
.frame(width: 300.0, height: 50.0)
.offset(y: -100.0)
}
}
}
.padding(300.0)
Other notes:
- Consider using
onGeometryChange
instead of aGeometryReader
:
Color.black
// .frame(minWidth: 300, minHeight: 50) // you can still add this if you want
.onGeometryChange(for: CGRect.self) {
$0.frame(in: .local)
} action: { newValue in
rect = newValue
}
.overlay {
ZStack {
Color.red.frame(width: rect.width, height: 2.0)
Color.blue.frame(width: 2.0, height: rect.height)
Color.green
.frame(width: 300.0, height: 50.0)
.offset(y: -100.0)
}
}
.padding(300.0)
Color
s naturally expand to their container’s size, so you don’t actually need aGeometryReader
here at all. I will assume that this is just a toy example and what you are actually doing does require aGeometryProxy
.frame(in: .local)
will almost always return a rect with origin (0, 0). Consider using aCGSize
if you just need the size.
EDIT Answer updated based on the assumption that this question concerns macOS only.
A GeometryReader
is greedy and will take all the space available. Then, the views inside the GeometryReader
are laid out inside this space, with alignment top-leading (that is, aligned to the top-left corner of the GeometryReader
).
I am using GeometryReader in SwiftUI to track the frame of a Color.black background
What you are actually tracking is the frame of the GeometryReader
in its own coordinate space (because .local
is used as coordinate space).
If in fact you want to know the size of the black rectangle then you can use .onGeometryChange
to read it instead. Although not apparent from the modifier’s name, this also measures the initial size on first show:
var body: some View {
ZStack {
Color.black
.onGeometryChange(for: CGRect.self) { geo in
geo.frame(in: .local)
} action: { frame in
// print("frame=(frame)")
rect = frame
}
Color.red.frame(width: rect.width, height: 2.0)
Color.blue.frame(width: 2.0, height: rect.height)
Color.green
.frame(width: 300.0, height: 50.0)
.offset(y: -100.0)
}
.padding(300.0)
}
The problem with this approach is that the window can only be made larger, not smaller. This is because the size of the ZStack
is constrained by the red and blue colors, which are still set to the rect
from before.
If you are simply trying to match the size of the red and blue rectangles to the black one, an easier approach is to use an overlay
. An overlay automatically adopts the size of the view it is applied to. So you no longer need a state variable to record the size of the black rectangle:
var body: some View {
ZStack {
Color.black
.overlay {
ZStack {
Color.red.frame(height: 2.0)
Color.blue.frame(width: 2.0)
}
}
Color.green
.frame(width: 300.0, height: 50.0)
.offset(y: -100.0)
}
.padding(300.0)
}
2