swiftswiftui

How to better align contents within a ZStack?


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!

Image


Solution

  • Keep in mind these basic rules of SwiftUI layout:

    1. The size of a container (such as an HStack, VStack or ZStack) is determined by its contents.

    2. Some views are greedy and consume as much space as possible. This includes all types of Color and Shape, and of course Spacer.

    3. 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)
        }
    

    Screenshot

    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)
    }
    

    Screenshot

    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.