swiftswiftuivstack

VStack children to fill (not expand) its parent


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)
        }
    )

Solution

  • 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.