Coming from UIKit I find it struggling to achieve desired results in SwiftUI. I assume I am to caught in the constraint driven layout to get my head around the SwiftUI approach.
Assume the following layout:
On devices with a "regular" screen the horizontal space should be split 1:1 into a top and bottom part.
The top container hold three views with fixed height which should be positioned at the top, center and bottom of the container.
The three views have some minimum spacing, e.g. 20px. If enough space is available the spacing can be more but never below 20px.
On smaller devices the top container can shrink up to a certain level. But never so small, that the fixed width of the subviews and their spacing would be violated. In this case the ratio between the top and bottom container would not be 1:1 anymore but e.g. 2:3.
On larger devices like an iPad the top container would only grow up to some max. height. In this case the ratio between top and bottom container would also not be 1:1 any more.
The bottom container view holds only one view which should always fill the available space.
Solving this using constraints in UIKit would be no problem. I am aware that "regular", "smaller" and "bigger" devices are no actual size classes. This is just for illustration.
But how would this be solved in SwiftUI?
The basic layout is of course straight forward:
VStack(spacing: 0) {
VStack {
TopView()
Spacer()
CenterView()
Spacer()
BottomView()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.red)
VStack {
FillView()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.green)
}
But how to achieve the min- and maxHeight of the top-container? Is it possible to do this without using a GeometryReader
but only with relative values?
How to avoid shrinking the top container below a given min-height when the bottom container becomes to big due to its content?
I think you can get quite close to your requirements with a few small changes:
VStack
with the Spacer
will add some (default) spacing in-between the views it contains, so this includes some additional spacing around the Spacer
. In order to enforce the 20 points minimum spacing, I would suggest using spacing: 20
on the VStack
, then remove the Spacer
and set maxHeight: .infinity
on CenterView
:VStack(spacing: 20) {
TopView()
CenterView()
.frame(maxHeight: .infinity)
BottomView()
}
maxHeight
on the upper VStack
, instead of .infinity
:VStack(spacing: 20) {
// ...
}
.padding()
.frame(maxWidth: .infinity, maxHeight: 400)
.background(.red)
FillView
, computed from the threshold height:FillView()
.frame(maxWidth: .infinity, minHeight: 200, maxHeight: .infinity)
.background(.green)
Note that the height of all iPhones in landscape orientation is smaller than even the smallest iPhone in portrait orientation.
Putting it all together:
VStack(spacing: 0) {
VStack(spacing: 20) {
TopView()
CenterView()
.frame(maxHeight: .infinity)
BottomView()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: 400)
.background(.red)
FillView()
.frame(maxWidth: .infinity, minHeight: 200, maxHeight: .infinity)
.background(.green)
}
Another way to distibute the space using ratios is to use a custom Layout
. An example implementation which distributes the views horizontally by their weights can be found in the answer to SwiftUI How to Set Specific Width Ratios for Child Elements in an HStack (it was my answer).