Issue #734
In SwiftUI there are fixed frame and flexible frame modifiers.
Fixed frame Positions this view within an invisible frame with the specified size.
Use this method to specify a fixed size for a view’s width, height, or both. If you only specify one of the dimensions, the resulting view assumes this view’s sizing behavior in the other dimension.
VStack {
Ellipse()
.fill(Color.purple)
.frame(width: 200, height: 100)
Ellipse()
.fill(Color.blue)
.frame(height: 100)
}
Flexible frame frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:)
Read the documentation carefully
Always specify at least one size characteristic when calling this method. Pass nil or leave out a characteristic to indicate that the frame should adopt this view’s sizing behavior, constrained by the other non-nil arguments.
The size proposed to this view is the size proposed to the frame, limited by any constraints specified, and with any ideal dimensions specified replacing any corresponding unspecified dimensions in the proposal.
If no minimum or maximum constraint is specified in a given dimension, the frame adopts the sizing behavior of its child in that dimension. If both constraints are specified in a dimension, the frame unconditionally adopts the size proposed for it, clamped to the constraints. Otherwise, the size of the frame in either dimension is:
If a minimum constraint is specified and the size proposed for the frame by the parent is less than the size of this view, the proposed size, clamped to that minimum.
If a maximum constraint is specified and the size proposed for the frame by the parent is greater than the size of this view, the proposed size, clamped to that maximum.
Otherwise, the size of this view.
Experiment with different proposed frame
To understand the explanation above, I prepare a Swift playground to examine with 3 scenarios: when both minWidth
and maxWidth
are provided, when either minWidth
or maxWidth
is provided. I use width
for horizontal dimension but the same applies in vertical direction with height
.
I have a View
called Examine
to demonstrate flexible frame. Here we have a flexible frame with red border and red text showing its size where you can specify minWidth
and maxWidth
.
Inside it is the content with a fixed frame with blue border and blue text showing content size where you can specify contentWidth
. Finally there’s parentWidth
where we specify proposed width to our red flexible frame.
The variations for our scenarios are that proposed width falls outside and inside minWidth
, contentWidth
, and maxWidth
range.
import SwiftUI
struct Examine: View {
let parentWidth: CGFloat
let contentWidth: CGFloat
var minWidth: CGFloat?
var maxWidth: CGFloat?
var body: some View {
Rectangle()
.fill(Color.gray)
.border(Color.black, width: 3)
.frame(width: contentWidth)
.overlay(
GeometryReader { geo in
Text("\(geo.size.width)")
.foregroundColor(Color.blue)
.offset(x: 0, y: -20)
.center()
}
)
.border(Color.blue, width: 2)
.frame(minWidth: minWidth, maxWidth: maxWidth)
.overlay(
GeometryReader { geo in
Text("\(geo.size.width)")
.foregroundColor(Color.red)
.center()
}
)
.border(Color.red, width: 1)
.frame(width: parentWidth, height: 100)
}
}
extension View {
func center() -> some View {
VStack {
Spacer()
HStack {
Spacer()
self
Spacer()
}
Spacer()
}
}
}
struct Examine_Previews: PreviewProvider {
static var previews: some View {
Group {
Group {
Text("Both minWidth and maxWidth")
Examine(parentWidth: 75, contentWidth: 150, minWidth: 100, maxWidth: 200)
.help("proposed size < min width")
Examine(parentWidth: 125, contentWidth: 150, minWidth: 100, maxWidth: 200)
.help("min width < proposed size < content")
Examine(parentWidth: 175, contentWidth: 150, minWidth: 100, maxWidth: 200)
.help("min width < content < proposed size")
Examine(parentWidth: 300, contentWidth: 150, minWidth: 100, maxWidth: 200)
.help("min width < content < max width < proposed size")
}
Group {
Text("Just minWidth")
Examine(parentWidth: 75, contentWidth: 150, minWidth: 100)
Examine(parentWidth: 125, contentWidth: 150, minWidth: 100)
Examine(parentWidth: 175, contentWidth: 150, minWidth: 100)
Examine(parentWidth: 175, contentWidth: 75, minWidth: 100)
.help("content < minWidth")
}
Group {
Text("Just maxWidth")
Examine(parentWidth: 75, contentWidth: 150, maxWidth: 200)
Examine(parentWidth: 125, contentWidth: 150, maxWidth: 200)
Examine(parentWidth: 175, contentWidth: 150, maxWidth: 200)
Examine(parentWidth: 300, contentWidth: 225, maxWidth: 200)
.help("content > maxWidth")
}
}
}
}
Observation
Here are the results with different variations of specifying parentWidth
aka proposed width.
🍑 Scenario 1: both minWidth and maxWidth are specified
Our red flexible frame clamps proposed width between its minWidth
and maxWidth
, ignoring contentWidth
let redWidth = clamp(minWidth, parentWidth, maxWidth)
🍅 Scenario 2: only minWidth is specified
Our red flexible frame clamps proposed width between its minWidth
and contentWidth
. In case content
is less than minWidth
, then final width is minWidth
let redWidth = clamp(minWidth, parentWidth, contentWidth)
🍏 Scenario 3: only maxWidth is specified
Our red flexible frame clamps proposed width between its contentWidth
and maxWidth
. In case content
is more than maxWidth
, then final width is maxWidth
let redWidth = clamp(contentWidth, parentWidth, maxWidth)
What are idealWidth and idealHeight
In SwiftUI, view takes proposed frame from its parent, then proposes its to its child, and reports the size it wants from it’s child and its proposed frame from parent. The reported frame is the final frame used by that view.
When we use .frame
modifier, SwiftUI does not changes the frame of that view directly. Instead it creates a container around that view.
There are 4 kinds of frame behavior depending on which View we are using. Some have mixed behavior.
- Sum up frames from its children then report the final frame. For example
HStack
,VStack
- Merely use the proposed frame. For example
GeometryReader
,.overlay
,Rectangle
- Use more space than proposed. For example texts with
fixedSize
- Use only space needed for its content and respect proposed frame as max
Fix the size to its ideal size
Some View like Text
or Image
has intrinsic content size, means it has implicit idealWidth
and idealHeight
. Some like Rectangle
we need to explicit set .frame(idealWidth: idealHeight)
. And these ideal width and height are only applied if we specify fixedSize
To understand this, let’s read fixedSize
Fixes this view at its ideal size. During the layout of the view hierarchy, each view proposes a size to each child view it contains. If the child view doesn’t need a fixed size it can accept and conform to the size offered by the parent. For example, a Text view placed in an explicitly sized frame wraps and truncates its string to remain within its parent’s bounds:
Text("A single line of text, too long to fit in a box.")
.frame(width: 200, height: 200)
.border(Color.gray)
The fixedSize() modifier can be used to create a view that maintains the ideal size of its children both dimensions:
Text("A single line of text, too long to fit in a box.")
.fixedSize()
.frame(width: 200, height: 200)
.border(Color.gray)
You can think of fixedSize() as the creation of a counter proposal to the view size proposed to a view by its parent. The ideal size of a view, and the specific effects of fixedSize() depends on the particular view and how you have configured it.
To view this in playground, I have prepared this snippet
struct Text_Previews: PreviewProvider {
static var previews: some View {
VStack(spacing: 16) {
Text("A single line of text, too long to fit in a box.")
.fixedSize()
.border(Color.red)
.frame(width: 200, height: 200)
.border(Color.gray)
}
.padding()
.frame(width: 500, height: 500)
}
}
Here we can see that our canvas is 500x500, and the Text
grows outside its parent frame 200x200
Play with Rectangle
Remember that shapes like Rectangle
takes up all the proposed size. When we explicitly specify fixedSize
, the idealWidth
and idealHeight
are used.
Here I have 3 rectangle
🍎 Red: There are no ideal size explicitly specified, so SwiftUI uses a magic number 10
as the size
🍏 Green: We specify frame directly and no idealWidth, idealHeight and no fixedSize, so this rectangle takes up full frame
🧊 Blue: The outer gray box has height 50, but this rectangle uses idealWidth
and idealHeight
of 200 because we specify fixedSize
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
VStack(spacing: 16) {
Rectangle()
.fill(Color.red)
.fixedSize()
Rectangle()
.fill(Color.green)
.frame(width: 100, height: 100)
Rectangle()
.fill(Color.blue)
.frame(idealWidth: 200, idealHeight: 200)
.fixedSize(horizontal: true, vertical: true)
.frame(height: 50)
.border(Color.gray)
}
.padding()
.frame(width: 500, height: 500)
}
}