currently learning Swift and attempting to recreate a simple banking app with SwiftUI. I'd like to know if you can align contents inside a ZStack in regards to the component behind it.
In this instance, I have the text over the white rectangle. When I add spacers or padding, it uses the edges of the screen to align, rather than the edges of the rectangle in which it sits on top of. Is there a way to use the background component as the alignment instead? Other than extracting my current code to be it's own view via a refactor, is there a different way to go about placing items on top of one another that is more efficient?
Oh, also, I can't seem to get the background to cover the screen no matter where I put it. Help there would also be appreciated. Thanks!
Keep in mind these basic rules of SwiftUI layout:
The size of a container (such as an HStack
, VStack
or ZStack
) is determined by its contents.
Some views are greedy and consume as much space as possible. This includes all types of Color
and Shape
, and of course Spacer
.
If you apply a .frame
modifier to a view (any view) then you are forcing it to fit within the size you specify. Sometimes you may be constraining it to fit within a smaller size, other times you may be enlarging it.
In your example, you are using a white RoundedRectangle
as the base for the view. By default, a shape like this will consume as much space as possible (see point 2 above). You are constraining it to a height of 100 by setting a .frame
(ref. point 3 above).
Although the HStack
(with the VStack
and the text) and the RoundedRectangle
are both contained inside the same ZStack
, they are unrelated. So the size of the HStack
is not in any way impacted by the height of the RoundedRectangle
, and vice versa.
If you are building a view with multiple layers, where one view should remain within the bounds of another view, it works well to use .overlay
or .background
for the dependent layer. An overlay layer and a background layer will adopt the frame of the view they are applied to.
So taking your example, if you want the text container to stay within the bounds of the RoundedRectangle
then you could apply it as an overlay:
var body: some View {
RoundedRectangle(cornerRadius: 8)
.fill(.white)
.frame(height: 100)
.overlay {
HStack {
VStack(alignment: .leading) {
// ...
}
.padding(30)
Spacer()
}
}
.padding()
.background(.black)
}
The order of modifiers is important. In the case here, the padding has been added to the RoundedRectangle
after the .overlay
, so the space on the left side of the text will be 30 (as this is the size of the padding being applied to the VStack
). If the padding on the rounded rectangle would be added before the overlay, the padding of the text would apply from the outer frame (with black background). In this case, the space on the left of the text would be reduced by the padding amount.
If in fact you want it to work the other way, with the RoundedRectangle
staying within the bounds of the text container, then you can add the RoundedRectangle
in the background. It is no longer necessary to set a fixed height on the RoundedRectangle
, because the background layer will adopt the frame of the view it is applied to (as explained above):
var body: some View {
HStack {
// ...
}
.background {
RoundedRectangle(cornerRadius: 8)
.fill(.white)
}
.padding()
.background(.black)
}
You will notice that this version has more height, because the padding of 30 around the VStack
is also having an impact top and bottom. This version will also change size automatically if the text needs to wrap, or if the user changes the text size on their device.
You were also asking, how to make the black background cover the full screen. Since it being applied as .background
to a view, the view itself needs to fill the screen. So you just need to set a frame with maxWidth: .infinity, maxHeight: .infinity
on the content, before adding the black background:
RoundedRectangle(cornerRadius: 8) // or HStack, for the second case
// ...
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity) // 👈 here
.background(.black)
Doing it this way, even the safe areas will be black too. This is because background(_:ignoresSafeAreaEdges:)
ignores the safe area edges by default. See this answer for more explanation.