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)

    }
}