SwiftUI: how to size to fit a Button to expand to fill a VStack or HStack parent View?

i0S Swift Issue

Question or problem in the Swift programming language:

I am trying to create 2 buttons that are equal width, positioned one above the other, vertically. It should look like this:

I have placed 2 Buttons inside a VStack which automatically expands to the width of the larger button. What I am trying to do is have the width of the buttons expand to fill the width of the VStack, but this is what I get instead:

VStack(alignment: .center, spacing: 20) {

    NavigationLink(destination: CustomView()) {
        Text("Button")
    }.frame(height: 44)
        .background(Color.primary)

    Button(action: { self.isShowingAlert = true }) {
        Text("Another Button")
    }.frame(height: 44)
        .background(Color.primary)

}.background(Color.secondary)

Setting the width of the VStack expands it, but the buttons do not expand to fit:

VStack(alignment: .center, spacing: 20) {
    ...
}.frame(width: 320)
    .background(Color.secondary)

So my question is:

Is there a way to do this, besides manually setting the frame of every item in my layout?

I would rather not have to specify each one as it will become difficult to manage.

How to solve the problem:

Solution 1:

Setting .infinity as the maxWidth, the frame(minWidth: maxWidth: minHeight:) API can be used to make a subview expand to fill:

VStack(alignment: .center, spacing: 20) {

    NavigationLink(destination: CustomView()) {
        Text("Button")
    }.frame(minWidth: 100, maxWidth: .infinity, minHeight: 44)
        .background(Color.primary)

    Button(action: { self.isShowingAlert = true }) {
        Text("Another Button")
    }.frame(minWidth: 100, maxWidth: .infinity, minHeight: 44)
        .background(Color.primary)

}.frame(width: 340)
    .background(Color.secondary)

enter image description here

Solution 2:

You will have to use the frame modifier with maxWidth: .infinity on the Text itself inside the button, this will force the Button to become as wide as it can:

VStack(alignment: .center, spacing: 20) {

    NavigationLink(destination: CustomView()) {
        Text("Button")
            .frame(maxWidth: .infinity, height: 44)
    }
    .background(Color.primary)

    Button(action: { self.isShowingAlert = true }) {
        Text("Another Button")
            .frame(maxWidth: .infinity, height: 44)
    }
    .background(Color.primary)

}.background(Color.secondary)

This works in iOS, but not in macOS using the default button style, which uses AppKit’s NSButton, since it refuses to get any wider (or taller). The trick in macOS is to use the .buttonStyle() modifier on your button (or the NavigationLink) and make your own custom button style like so:

struct MyButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .background(configuration.isPressed ? Color.blue : Color.gray)
    }
}

The reason why you must apply the frame modifier to the Text view and not the button itself, is that the button will prefer to stick to its content’s size instead of the size suggested by the view that contains the button. What this means is that if you apply the frame modifier to the button and not the Text inside it, the button will actually remain the same size as the Text and the view returned by .frame is the one that will expand to fill the width of the view that contains it, so you will not be able to tap/click the button outside the bounds of the Text view.

Hope this helps!