4

i am trying to to make the button of an alert view fit the parent VStack. But I can only see two options:

  1. button width as is, no frame modifier. that is not ideal as the button is not wide enough Alert without max width

  2. set the frame modifier to .frame(maxWidth: .infinity). that is not ideal, because it not also fills its parent, but also makes it extend to the edges of the screen. Alert with max width

What I actually want is, that the VStack stays at its width and the button just fills up to the edges. No extending of the VStack. The size of the VStack is defined by the title and message, not by the button. Is this possible to achieve with SwiftUI?

Code:

Color.white
    .overlay(
        ZStack {
            Color.black.opacity(0.4)
                .edgesIgnoringSafeArea(.all)

            VStack(spacing: 15) {
                Text("Alert View")
                    .font(.headline)
                Text("This is just a message in an alert")
                Button("Okay", action: {})
                    .padding()
                    .frame(maxWidth: .infinity)
                    .background(Color.yellow)
            }
            .padding()
            .background(Color.white)
        }
    )
warly
  • 1,458
  • 1
  • 16
  • 21

2 Answers2

3

As alluded to in the comments, if you want the width to be tied to the message size, you'll have to use a PreferenceKey to pass the value up the view hierarchy:

struct ContentView: View {
    
    @State private var messageWidth: CGFloat = 0
    
    var body: some View {
        Color.white
            .overlay(
                ZStack {
                    Color.black.opacity(0.4)
                        .edgesIgnoringSafeArea(.all)
                    
                    VStack(spacing: 15) {
                        Text("Alert View")
                            .font(.headline)
                        Text("This is just a message in an alert")
                            .background(GeometryReader {
                                Color.clear.preference(key: MessageWidthPreferenceKey.self,
                                                       value: $0.frame(in: .local).size.width)
                            })
                        Button("Okay", action: {})
                            .padding()
                            .frame(width: messageWidth)
                            .background(Color.yellow)
                    }
                    .padding()
                    .background(Color.white)
                }
                .onPreferenceChange(MessageWidthPreferenceKey.self) { pref in
                    self.messageWidth = pref
                }
            )
    }
    
    struct MessageWidthPreferenceKey : PreferenceKey {
        static var defaultValue: CGFloat { 0 }
        static func reduce(value: inout Value, nextValue: () -> Value) {
            value = value + nextValue()
        }
    }
}

I'd bet that there are scenarios where you would also want to set a minimum width (like if the alert message were one word long), so a real-world application of this would probably use max(minValue, messageWidth) or something like that to account for short messages.

jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • yep, crazy verbose solution, but seems to be the only way right now. thank you – warly May 12 '21 at 13:44
  • Hmm this seems extremely complicated for such a trivial thing. Do you know of any other solution? – aheze Dec 20 '21 at 23:15
1

According to https://sarunw.com/posts/swiftui-fixedsize, applying .fixedSize() on a VStack will "constrain its size large enough to hold the ideal sizes of each button."

So, using it in conjunction with .frame(maxWidth: .infinity) like so:

Color.white
    .overlay(
        ZStack {
            Color.black.opacity(0.4)
                .edgesIgnoringSafeArea(.all)

            VStack(spacing: 15) {
                Text("Alert View")
                    .font(.headline)
                Text("This is just a message in an alert")
                Button("Okay", action: {})
                    .padding()
                    // 1:
                    .frame(maxWidth: .infinity)
                    .background(Color.yellow)
            }
            .foregroundColor(.black)
            // 2:
            .fixedSize()
            .padding()
            .background(Color.white)
        }
    )

...results in:
enter image description here