Richard D
Richard D

Reputation: 5665

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

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

Two equal-width buttons positioned vertically with space between them

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 auto sizing to largest button

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)

VStack width expands but Buttons do not

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.

Upvotes: 29

Views: 26454

Answers (4)

Mojtaba Hosseini
Mojtaba Hosseini

Reputation: 119686

πŸ’‘iOS 17

From iOS 17, the correct way to do this is to use .containerRelativeFrame modifier because using .infinity width on the frame actually pushes the parent's frame to have an infinite width.

So it would be like:

Button { } label: {
    Text("Match Parent")
        .foregroundStyle(.white)
        .containerRelativeFrame(.horizontal) // πŸ‘ˆ This should be inside the `label` parameter of the `button`. Otherwise it would be NOT touchable
        .padding(-10) // πŸ‘ˆ This is here to make space to the edges. Yes it should have a NEGATIVE value
}
.frame(height: 56)
.background(.red)
Demo:

demo

Upvotes: 10

Jared
Jared

Reputation: 953

If you want the match the dynamic width of the VStack, ie: only take the space it needs (based on the dynamic width of the child views). You can do this by adding the .fixedSize(horizontal:,vertical:) modifier to the parent VStack.

Example:

var body: some View {
    VStack {
        Button("Log in") { }
            .foregroundColor(.white)
            .padding()
            .frame(maxWidth: .infinity)
            .background(.red)
            .clipShape(Capsule())
        
        Button("Reset Password") { }
            .foregroundColor(.white)
            .padding()
            .frame(maxWidth: .infinity)
            .background(.red)
            .clipShape(Capsule())
    }
    // Tells SwiftUI that the child views should only take up the space they need.
    .fixedSize(horizontal: true, vertical: false)
}

Match dynamic width in VStack

Upvotes: 2

Ricardo Nogueira
Ricardo Nogueira

Reputation: 103

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.

Upvotes: 10

Richard D
Richard D

Reputation: 5665

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

Upvotes: 29

Related Questions