In my first post I showed that in SwiftUI views choose their own sizes, derived from the size of their content, but that that they can be flexible about what size they choose based on the size proposed by their parent.
In the post about stacks we showed that the size chosen by a stack is derived from the sizes of its children, and reinforced why it’s important not to confuse the proposed size a child receives from its parent with the size later chosen by its parent.
The layout process looks like:
- Parent proposes a size to its child.
- Child chooses its own size.
- Parent chooses its own size based on the child, and its own constraints.
- Parent positions the child within its bounds.
I also showed that while you cannot override the chosen size of a view, you can use this process to influence the result by using views such as frames.
A frame is added to a view using the .frame
view modifier; it creates
a new view which both proposes the size given to its children, and
chooses the size given as its own size, finally positioning the child
view it was added to within its own bounds.
It’s important to remember that the modifier does not directly change
the bounds of the view being modified in anyway. We can demonstrate this
using inflexible views such as Image
; firstly by placing one in a
frame that’s too big:
struct ContentView : View {
var body: some View {
Image("barbarian")
.frame(width: 200, height: 200)
}
}
Which we should recall shows that while the frame proposes a larger size to its child, the child still chooses its size based on its source image, and all that’s left to do for the frame is position it:
And likewise if we place the Image
in a frame that’s too small:
struct ContentView : View {
var body: some View {
Image("barbarian")
.frame(width: 100, height: 200)
}
}
The Image
’s chosen size is not overridden by the frame, and exceeds
its bounds:
This latter has a particularly interesting behavior; since the parent of
the frame is only aware of the size chosen by the frame, from the point
of view of its siblings and all other views, the size of the Image
is
irrelevant for layout purposes.
Layout Neutral Views
So far in all our frame examples we’ve passed a fixed value to the
width
and height
parameters, but if you take a look at the API you’d
see that these are both optional.
func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment = .center) -> some View
What happens if we omit one? For example, the height:
struct ContentView : View {
var body: some View {
Image("barbarian")
.resizable()
.frame(width: 50)
}
}
Note first that we made our Image
resizable, which we should recall
has it ignore the size of its source content, and instead choose the
size proposed by the parent.
We’ve placed the Image
inside a .frame
which will be its parent, and
we’ve proposed a width for that parent, but we haven’t proposed a
height:
What we see is that the image has the width we specified, taking the proposed width from its parent frame, but is as high as there is space available in the device, using the height proposed by the frame’s own parent.
Likewise we see that the frame for the image has that same height.
When a view simply passes the proposed size it receives from its own parent down to its children without modification, and then chooses as its own size the size that its own child chose, we say that the view is layout neutral. Simply: when a view is layout neutral, the size of the view is the same as the size of its child.
when a view is layout neutral, the size of the view is the same as the size of its child
We’ve been using layout neutral views all the time probably without realizing it.
The Image
returned in our examples is the child of its frame, which is
made to be the content of a view by returning it as the value of the
computed ContentView.body
property. But ContentView
itself is also a
view, and is the parent to its body, and child to its parent, the
window.
We’ve never had to concern ourselves with this because ContentView
is
layout neutral, so invisible to layout.
So our frame is fixed in width, but layout neutral in height.
Unsurprisingly it works the other way too, when a frame is fixed in height but layout neutral in width:
struct ContentView : View {
var body: some View {
Image("barbarian")
.resizable()
.frame(height: 50)
}
}
The Image
becomes as wide as is proposed by the frame’s parent to the
frame:
Flexible Frames
Now that we’ve more fully explored what we can do with the .frame
modifier that lets us specify fixed widths and heights, it’s time to
look at the other .frame
modifier available to us:
func frame(minWidth: CGFloat? = nil, idealWidth: CGFloat? = nil, maxWidth: CGFloat? = nil, minHeight: CGFloat? = nil, idealHeight: CGFloat? = nil, maxHeight: CGFloat? = nil, alignment: Alignment = .center) -> some View
There are a lot of options here, allowing us to constrain the width and height in three ways:
- minimum length
- ideal length
- maximum length
When any option is nil
the behavior is to be layout neutral for that
characteristic, while remaining constrained by the others you provide.
when any option is `nil` the behavior is to be layout neutral for that characteristic
In order to fully understand these we have to remember that the frame is a view in its own right, and only influences the size of its child view through the size it proposes to that child, and is itself influenced only by the size proposed by its own parent.
We can expand the layout process to include all three views:
- Parent proposes a size to the frame.
- Frame applies constraints to that size.
- Frame proposes the constrained size to the child.
- Child chooses its own size.
- Frame chooses its own size size based on the child, and its own constraints.
- Frame positions the child within its bounds.
- Parent positions the frame within its bounds.
We’ll show three frames in our examples, with the gray border being a
parent frame supplied equivalent to the device in our other examples,
and we’ll concentrate on the width
parameters to show behavior, with
the height
being understood as behaving equivalently.
Minimum Length
The minimum length characteristic first comes into play when the frame applies constraints to the size proposed by its parent. The child receives a proposed length that is the larger of the parent’s length, and the frame’s minimum length.
This is easiest to demonstrate when the parent is too small:
struct ContentView : View {
var body: some View {
// parent frame width is 50
Image("barbarian")
.resizable()
.frame(minWidth: 100)
}
}
The frame receives a proposed width of 50 from the parent, but has a minimum width constraint of 100; thus the child image receives a proposed width of 100 from the frame.
Both the image and the frame overflow the parent:
The minimum length also applies to the frame choosing it’s own size based on that of the child. If the child chooses a length that is smaller than the frame’s minimum length, the frame’s length becomes its minimum length and the child is positioned inside it.
struct ContentView : View {
var body: some View {
// image width is 101
Image("barbarian")
.frame(minWidth: 200)
}
}
The frame receives a proposed width from the parent, and applies the minimum width constraint of 200; which the child receives as its proposed size.
The child image is not resizable, so chooses a width of 101; the frame still applies its own minimum width, so chooses 200 and centers the image within that space:
Maximum Length
Like with the minimum length, the maximum length characteristic applies both to the size proposed by the parent, and the frame’s choice as to its own size after the child has chosen.
For the maximum length, the child receives a proposed length that is the smaller of the parent’s length and the frame’s maximum length.
This is easiest to demonstrate when the parent is too large:
struct ContentView : View {
var body: some View {
// parent frame width is 200
Image("barbarian")
.resizable()
.frame(maxWidth: 100)
}
}
The frame receives a proposed width of 200 from the parent, but has a maximum width constraint of 100; thus the child image receives a proposed width of 100 from the frame.
The parent centers the resulting frame within its bounds:
The maximum length also applies to the frame choosing its own size, based on that of the child. If the child chooses a length that is larger than the frame’s maximum length, the frame’s length is still constrained to its maximum length, and the child overflows it:
struct ContentView : View {
var body: some View {
// image width is 101
Image("barbarian")
.frame(maxWidth: 50)
}
}
The frame receives a proposed width from the parent, and applies the maximum width constraint of 50; which the child receives as its proposed size.
The child image is not resizable, so chooses a width of 101; the frame still applies its own maximum width, so chooses 50 for its own width, and the image overflows:
Ideal Length
The ideal length is used within situations where the parent is unable to
propose a size to the frame, for example within a ScrollView
,
specifying the length to choose in that situation.
Image
has ideal lengths of its source image, even when resizable,
Text
has ideal lengths based on its string content rendered on a
single line.
Setting ideal lengths tends to be necessary when creating flexibly-sized
shapes and other custom views, for example by using [geometry
readerhttps://netsplit.com/swiftui/geometry-reader/), that are going
to be used inside ScrollView
.
The default values for the ideal length are nil
which means that the
ideal length will be layout neutral, and be the length of the child. We
can use this combined with fixedSize
to cause frame’s to hug their
children while proposing larger possible sizes, see size-limiting
frames for an
example.
Layout Non-Neutral Frames
So far we’ve only needed to specify one of the minimum or maximum lengths individually, the default behavior of being layout neutral usually sufficing for the unspecified length.
We can specify a minimum and maximum constraint together as long as they are in ascending order, this not only constrains the frame length within the given bounds, but also stops the frame from being layout neutral in that dimension.
Put another way, setting both a minimum and maximum length for a dimension bases the frame length on its parent, not its child.
setting both a minimum and maximum length for a dimension bases the frame length on its parent, not its child
For example we can constrain a frame to between the width of its parent and the width of its non-resizable child:
struct ContentView : View {
var body: some View {
// image width is 101, parent frame width is 200
Image("barbarian")
.frame(minWidth: 50, maxWidth: 150)
}
}
In this case since the proposed width from the frame’s parent is 200, and that does not fall in the range of 50–150, the child receives a proposed width of 150 instead. Since the child image is not resizable it chooses a width of 101.
This is where things now differ; if we had specified nil
for either
the minimum or maximum width, the frame would have had the potential of
being layout neutral and able to choose its width as equal to the width
of the child.
But by providing both, the frame can no longer be layout neutral, and instead always chooses its width based on the proposed width it received from its own parent. Since the proposed width was 200, and the maximum width of the frame is 150, the frame’s width chosen to be 150, and the child is positioned inside that:
We can also see what happens if the range of the constraints include the size of the parent frame:
struct ContentView : View {
var body: some View {
// image width is 101, parent frame width is 200
Image("barbarian")
.frame(minWidth: 50, maxWidth: 250)
}
}
In this case the parent width of 200 falls within the range 50–250, so the child receives a proposed width of 200. Again the child image is not resizable, so it chooses a width of 101.
Since both a minimum and maximum width are specified, the frame is not layout neutral, and chooses its width based on the proposed width from its own parent. As that’s within its constraints, it chooses 200 and positions the child inside that:
We should round out by considering the case where the parent length is smaller than the minimum length of the frame:
struct ContentView : View {
var body: some View {
// image width is 101, parent frame width is 200
Image("barbarian")
.frame(minWidth: 225, maxWidth: 500)
}
}
All rules learned thus far are observed.
The proposed width the child becomes the frame’s minimum width and the child chooses a smaller width. Since the frame is not layout neutral it chooses its minimum width as its own width, since that is closest to that of the proposed width from its parent.
Within its chosen width, it positions its child, and overflows its parent’s bounds:
Infinite Frames
Since specifying both the minimum and maximum length makes the frame
sized based on the parent, constrained by the range given, an
interesting case occurs when you pass 0 as the minimum length and
.infinity
as the maximum length:
struct ContentView : View {
var body: some View {
// image width is 101, parent frame width is 200
Image("barbarian")
.frame(minWidth: 0, maxWidth: .infinity)
}
}
We can work through this as we have other examples.
The parent proposes a width of 200 to the frame, which is within the frame’s constraints of 0–∞, so the frame proposes a width of 200 to the child. The child image is still not resizable, so chooses 101 as its width.
Because both a minimum and maximum width are specified, the frame is not layout neutral. Since the proposed width from the parent of 200 is greater than 0 and less than ∞, the frame chooses its own width to be 200 and it positions the child inside that:
In other words, when specified with a minimum width of 0, and a maximum
width of .infinity
, the frame always has the size of its parent.
when specified with a minimum width of 0, and a maximum width of `.infinity`, the frame has the size of its parent
Both are required, because if we’d specified only the minimum or maximum width, the frame would have been potentially layout neutral and chosen the width of its child rather the proposed width from its parent.
Infinite frames can be a convenient way to adjust the alignment of a view within the bounds of a parent of unknown alignment, for example:
struct ContentView : View {
var body: some View {
// always layout top-leading, in any frame
Image("barbarian")
.frame(minWidth: 0, maxWidth: .infinity,
minHeight: 0, maxHeight: .infinity,
alignment: .topLeading)
}
}
Ordinarily as a construct this would be unnecessary since you could simply change the alignment of the surrounding frame, but it’s useful example of how the infinite frame works.
In both width and height the proposed length from the parent falls within the 0–∞ bounds, so the child of the frame receives the proposed size from the frame’s parent as a proposed size.
Since both minimum and maximum lengths are specified, the frame is not layout neutral, so chooses its size as the proposed size from its parent, not the size chosen by its child.
Finally it positions the child, using the alignment specified when creating the frame: