Consider the following (in a landscape-only app):
struct ContentView: View {
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.blue)
.frame(width: UIScreen.main.bounds.size.width * 0.9, height: UIScreen.main.bounds.size.height * 0.9)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea(.all)
.background(.gray)
}
}
Why is the rectangle not centered? Why is there more space on the right than on the left? Happens on a device too.
This is an interesting one. If you change the scaling factor from 0.9 to 0.8 then the blue area is centered perfectly. But when you set a frame size that needs to use the safe areas to fit, the position is not centered.
I did some measurements of the screen size of an iPhone 15 in landscape mode (which is what you appear to be using in your screenshot):
The sizes delivered by the (deprecated) UIScreen.main.bounds.size
are the full screen sizes. So when a factor of 0.9 is applied, the size of the blue area is being fixed at 766.8 x 353.7. This fits within the available height (of 372), but not within the available width (of 734). The excess width is 32.8 pt.
It is perhaps useful to note that the gray background is giving the impression that the ZStack
is filling the screen, but this is not the case. You have applied the background using .background(.gray)
, which ignores safe areas by default - see
background(_:ignoresSafeAreaEdges:). If you change it to .background { Color.gray }
then safe areas are not ignored and the background shows you the exact size and position of the ZStack
. The result is quite interesting:
What we see is:
ZStack
is still observing the safe areas as far as possible.ZStack
exactly matches the width of the blue area, because the blue content is forcing it to be wider than the area within the safe insets. So the ZStack
has expanded into both the leading and trailing safe areas by half the excess width or 16.4 pt on both sides.ZStack
. This content is being vertically centered within the full screen height, so the gap to the bottom of the screen is (393 - 353.7) / 2 = 19.65. This is slightly less than the bottom safe inset of 21.I think that what is happening is that the ZStack
is calculating the horizontal offset for the content based on the assumption that the ZStack
is in its default position. So the relative offset is half the excess width or 16.4 pt. But since the ZStack
itself is already displaced by this much, it means the blue content has a double displacement. The result is that the correction for the excess width is fully applied on the leading side and the gap on the trailing side is the default safe area inset.
1. Only ignore safe areas on the top and bottom edges
ZStack {
Rectangle()
.foregroundColor(.blue)
.frame(width: UIScreen.main.bounds.size.width * 0.9, height: UIScreen.main.bounds.size.height * 0.9)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea(edges: [.top, .bottom]) // <- HERE
.background(.gray)
The effect of this change is to stop the ZStack
from applying an offset to the content, so the horizontal position of the ZStack
becomes the horizontal position of the content too (they are horizontally aligned and also horizontally centered).
This workaround is based on the assumption that the leading and trailing safe area insets are always matched. However, this might not be the case on all devices.
2. Apply maximum size to the ZStack
again after the modifier .ignoresSafeArea
ZStack {
Rectangle()
.foregroundColor(.blue)
.frame(width: UIScreen.main.bounds.size.width * 0.9, height: UIScreen.main.bounds.size.height * 0.9)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea()
.frame(maxWidth: .infinity, maxHeight: .infinity) // ADDED
.background(.gray)
The first frame modifier on the ZStack
extends the ZStack
up to the safe areas. After ignoring safe areas, the second frame modifier extends the ZStack
to the screen size.
3. Apply max size and ignoresSafeArea
to the blue rectangle
ZStack {
Rectangle()
.foregroundColor(.blue)
.frame(width: UIScreen.main.bounds.size.width * 0.9, height: UIScreen.main.bounds.size.height * 0.9)
.frame(maxWidth: .infinity, maxHeight: .infinity) // ADDED
.ignoresSafeArea() // ADDED
}
.ignoresSafeArea()
.background(.gray)
A frame with max size no longer needs to be set on the ZStack
, because its size is dictated by the content. But the ZStack
still needs to ignore safe areas.
4. Use a GeometryReader
as the parent container
Using a GeometryReader
is a better way of getting the screen size (because UIScreen.main
is deprecated and it doesn't work properly with split screen on iPad). If the modifier .ignoresSafeArea
is moved from the ZStack
to the GeometryReader
then the ZStack
extends to the full screen size when the frame with max size is applied:
GeometryReader { proxy in
ZStack {
Rectangle()
.foregroundColor(.blue)
.frame(width: proxy.size.width * 0.9, height: proxy.size.height * 0.9)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.gray)
}
.ignoresSafeArea()
I would suggest, this is the best workaround.
All workarounds give the same result: