Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 465
cleverbit Avatar asked Jul 19 '19 09:07

cleverbit


2 Answers

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

like image 170
cleverbit Avatar answered Sep 28 '22 15:09

cleverbit


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.

like image 33
Ricardo Nogueira Avatar answered Sep 28 '22 15:09

Ricardo Nogueira