It seems that a List reports its ideal height as 0, which is not very helpful. This prevents .fixedSize from being used to restrict it to its ideal height. A custom Layout that performs its layout based on the ideal height of its content will also not work (which is a shame).
However, another way to find the height needed by the List is to use a GeometryReader to examine the relative position of the items inside the list, in particular, the footer. This can then be used to set a maxHeight for the List:
@State private var maxListHeight = CGFloat.infinity
var body: some View {
GeometryReader { outer in
VStack {
List {
Section {
Text("Row 1")
Text("Row 2")
} header: {
Text("Header")
.font(.title3)
.padding()
} footer: {
Text("Footer")
.font(.body)
.padding()
.background {
GeometryReader { inner in
Color.clear
.onAppear {
let maxY = inner.frame(in: .global).maxY
maxListHeight = maxY - outer.safeAreaInsets.top
}
}
}
}
}
.listStyle(.insetGrouped)
.frame(maxHeight: maxListHeight)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.environment(\.defaultMinListRowHeight, 50)
}
}

You will notice that the global coordinate space is being used to find the position of the footer, so the height of the List is computed by deducting the height of the top safe-area insets. These insets are measured by another GeometryReader that surrounds the VStack.
I was thinking that the outer GeometryReader could be avoided by using the coordinate space of the List. However, I discovered that the items in the List (or at least, the section footer) cannot reference the coordinate space of the container, so it always defaults to the global coordinate space.
What this means, is that if the List is not the first item in the layout then you would need to deduct the height of whatever comes before it when computing the height of the List from the position of the footer.




