iosswiftswiftuivstack

Which rules does VStack use to allocate space between subviews?


TL;DR Which rules does VStack use to distribute available space between its subviews. especially when maxHeight: .infitiy is involved.

Coming from UIKit and LayoutConstraints I still struggle to get my head around how SwiftUI sizes its view.

After reading / watching different tutorials, docs, etc. I understand that ultimately each view determines its size itself. The parent view offers the available size, and the child decides how big it wants to be. For instance this WWDC video shows this with nice examples.

However, how does this work in the following VStack:

VStack(spacing: 0) {
    VStack {
        Text("Top")
           .background(.blue)
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .background(.red)
    
    VStack {
        Text("Bottom")
           .background(.yellow)
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .background(.green)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)

Is this correct?

Text("Bottom")
   .frame(maxWidth: .infinity, maxHeight: .infinity)
   .background(.yellow)

Now the bottom label requests infinite height for itself. However the inner VStack only gives it the available 50% of the screen height.

But when adding more Subviews to the inner VStacks the 50% limit does not hold:

VStack(spacing: 0) {
    VStack {
        Text("Top1")
        Text("...")
        Text("TopN")
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .background(.red)
    
    VStack {
        Text("Bottom1")
        Text("...")
        
        // ...
        
        Text("BottomN")
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .background(.green)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)

How is the Size calculated here? Why does the bottom VStack becomes bigger than 50% in this case but not before?

enter image description here


Edit:

I am surprised that the behaviour on the third screenshot cannot be reproduced.

Here is another screenshot showing the complete code and the result:

enter image description here


Solution

  • Edited answer for the edited question:

    Here is a detailed description of how the layout process works in your case:

    1. Outer VStack gets the min size of the inner VStacks (zero proposal)

    The inner VStacks do the same to get the min size of their child views. In your example, this is the sum of the min sizes of the Text views within the inner VStacks.

    2. Outer VStack gets the max size of the inner VStacks (infinity proposal)

    Since you're using frame(maxHeight: .infinity) the inner VStacks will respond with an infinity value.

    3. Calculations based on the size available to the outer VStack

    After steps 1 and 2, the outer VStack has all the information it needs for the calculation.

    a) If the minimum height of both inner VStacks is less than 50% of the available height, the outer VStack will propose a specific height of 50% to each of the inner VStacks.

    The inner VStacks will then accept this suggestion as the suggested value is within their min/max range.

    b) If one of the inner VStacks returns a minimum height that is greater than 50% of the available height, the outer VStack makes a corresponding distribution as a suggestion to the inner VStacks.

    Let's take a concrete example for this case:

    As a result, the outer VStack suggests a height of 700 for the green VStack and a height of 100 (800 minus 700) for the red VStack.

    The strategy of a VStack is therefore to divide the available space as evenly as possible, taking into account the respective min/max heights of the child views.

    By using frame(maxHeight: .infinity) you do not prevent one of the views from being reduced in size if required. This is probably where your misunderstanding lies.

    Using this simply tells the outer VStack in step 2 described above that the inner VStacks have no maximum height. If you were to use a specific value instead of infinity, this would be taken into account in step 3.


    Note from the original answer, which is still valid

    Regarding your general questions about how SwiftUI's layout system or more specifically SwiftUI layout containers work, I recommend you take a look at how to implement your own layout container.

    The documentation of this can be found here.

    Implementing your own layout container for learning purposes also allows you to look over the shoulder of the SwiftUI layout system in detail.

    In particular, the sizethatfits and placeSubviews methods to be implemented for a layout container should give you a detailed insight.

    You can also put a custom layout container into a VStack, for example to observe how a VStack interacts with its child views via the mentioned method calls.

    Note in particular that, depending on the implementation, a layout container may ask its child views several times via sizethatfits using different proposals to determine the ideal size:

    The container’s parent view that calls this method might call the method more than once with different proposals to learn more about the container’s flexibility before deciding which proposal to use for placement.