SwiftUI: Different alignment behavior of containers in GeometryReader

488 views Asked by At

A Text element inside a ZStack(alignment: .bottom) is displayed at the bottom of the container (as expected). Tho, if the same ZStack is inside a GeometryReader, the alignment does not behave the same anymore.

This code:

var body: some View {
  ZStack(alignment: .bottom) {
    VStack {
      Text("Outside geo")
    }
  
    GeometryReader { geo in
      ZStack(alignment: .bottom) {
        VStack {
          Text("Inside geo")
        }
      }.frame(maxWidth: .infinity, maxHeight: .infinity)
    }
  }.frame(maxWidth: .infinity, maxHeight: .infinity)
}

produces this result:

Result of code

If I'd want the "inside geo" to be placed at the bottom too, I'd have to add alignment: .bottom inside its parent ZStack .frame(...) modifier.

What's the background for this behavior? Why can I use ZStack(alignment: ...) in one case, but have to use ZStack {...}.frame(alignment: ...) in the other to achieve the same result?

2

There are 2 answers

0
Dominik Seemayr On

After experimenting, I now know what causes this behavior, still it felt kinda unexpected first.

When setting the .frame max-size of the ZStack to .infinity (to stretch it out), its "inner size" / content still stays "hugged", rather than stretched out too. Means, the content of a container does not always necessarily use all the space it could.

Take this example:

ZStack(alignment: .bottom) {
  Color.red
    .frame(width: 50, height: 50)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)

This produces a whole different placement of the red box than when adding a second view, that pushes the inner bounds of the container.

ZStack(alignment: .bottom) {
  Color.red
    .frame(width: 50, height: 50)
  
  Color.clear
    .frame(maxHeight: .infinity)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)

The frame of the clear color now pushes its parents bounds, since its height tries to scale up as much as it can. A GeometryReader has the same nature, since it always tries to fill its parent.

Now the red box is not hugged anymore and has all the space (caused by the clear color) to move around freely (to the bottom).

Here are the two results:

enter image description here

Conclusio

The alignment parameter of .frame decides where the hugged "inner frame" of a container is placed inside the full-frame of the container. The alignment parameter of a Container decides, where its content is placed inside its "inner frame" (which can be hugged).

0
HunterLion On

It might look strange, but the text "Inside geo" in the GeometryReader is correctly aligned at the bottom of the ZStack - the problem is that your ZStack is not aligned at the bottom of the GeometryReader.

The stacks occupy the minimum amount of space necessary, while the GeometryReader has the opposite behaviour. So, the GeometryReader is aligned at the bottom of your high-level ZStack but it occupies the whole screen, while the inner-level ZStack is centred inside the GeometryReader.

Try putting a .background(someColor) on each view and you'll see where they are positioned.

If you want the "Inside geo" to be at the bottom of the screen, you need to tell the ZStack to get placed at the bottom with the alignment parameter - like this:

    var body: some View {
        ZStack(alignment: .bottom) {
            VStack {
                Text("Outside geo")
            }
            
            GeometryReader { geo in
                ZStack(alignment: .bottom) {
                    VStack {
                        Text("Inside geo")
                    }
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) // Here you add the alignment parameter
            }

        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }