swiftui

A View given a maxWidth automatically takes up the maxWidth of space. Is there a way to get it to take up only as much as it needs?


I am trying to create buttons which grow up to a maximum width if necessary at which point they use "..." to truncate their title. I know there are some unusual things about button frames, like the difference in applying frame before or after .buttonStyle however this behavior is surprising to me.

It seems applying the maxWidth makes the button take up that width even if it doesn't need it. If you apply a button style that gives it a background it would have whitespace around the button.

I have found this applies to Text() as well.

struct TestButton: View {
    let title: String

    var body: some View {
        Button {
            print(title)
        } label: {
            Text(title)
                .lineLimit(1)
        }
        .frame(maxWidth: 180)
        .background(.purple)
        .buttonStyle(PlainButtonStyle())
        .focusable()
    }
}
HStack {
    TestButton(title: "Bears")
    TestButton(title: "Battlestar Galactica")
}

enter image description here


Solution

  • .frame(maxWidth: 180) is simply wrapping an invisible "frame" around the button. This frame is very flexible - if it receives a size proposal with a width of at most 180pt, it will choose its width to be the same as the proposed width. Otherwise, it will choose its width to be 180pt, effectively "capping" the proposal.

    This is exactly what HStack proposes to the frame. HStack will first try to propose an equal amount of width to each children, trying to lay them out evenly. The frames accept these proposals, because the HStack happens to be less than 360pt wide*.

    You can add fixedSize() (or fixedSize(horizontal:vertical:)) to each of the buttons, so that instead of proposing a specific width to the buttons, an .unspecified proposal is sent to the frames. The frame will forward this to the buttons and the buttons will choose a size that fits their content. Of course, if the width they chose is larger than 180pt, it will still be capped.

    Adding fixedSize() to the HStack also works, because HStack will also forward the .unspecified proposal to its children, instead of trying to space the children out evenly (it literally cannot do this because there is no proposed width).

    However, fixedSize() will not allow the buttons to shrink either. If the HStack is too narrow, the buttons will exceed the bounds of the HStack. If this is undesirable, you might need to write your own Layout. While it is relatively clear what you want to do in the case of 2 views, it's not very clear how you want this to work in general.


    * More accurately, the HStack itself received a size proposal with a width < 360pt + default spacing of the HStack.