Reputation: 5665
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.
Upvotes: 29
Views: 26454
Reputation: 119686
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)
Upvotes: 10
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)
}
Upvotes: 2
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
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)
Upvotes: 29