Search code examples
iosswiftswiftuihstack

SwiftUI How to align child views inside HStack


I have a similar problem to this question (no answer yet): SwiftUI HStack with GeometryReader and paddings

In difference my goal is to align two views inside an HStack and where the left view gets 1/3 of the available width and the right view gets 2/3 of the available width.

Using GeometryReader inside the ChildView messes up the whole layout, because it fills up the height.

This is my example code:

struct ContentView: View {
    var body: some View {
        VStack {
            VStack(spacing: 5) {
                ChildView().background(Color.yellow.opacity(0.4))
                ChildView().background(Color.yellow.opacity(0.4))
                Spacer()
            }

            .padding()

            Spacer()

            Text("Some random Text")
        }
    }
}

struct ChildView: View {
    var body: some View {
        GeometryReader { geo in
            HStack {
                Text("Left")
                    .frame(width: geo.size.width * (1/3))
                Text("Right")
                    .frame(width: geo.size.width * (2/3))
                    .background(Color.red.opacity(0.4))
            }
            .frame(minWidth: 0, maxWidth: .infinity)
            .background(Color.green.opacity(0.4))
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Which results in this:

enter image description here

Now If you would embed this view inside others views the layout is completely messed up:

e.g. inside a ScrollView

enter image description here

So how would one achieve the desired outcome of having a HStack-ChildView which fills up the space it gets and divides it (1/3, 2/3) between its two children?

EDIT

As described in the answer, I also forgot to add HStack(spacing: 0). Leaving this out is the reason for the right child container to overflow.


Solution

  • You can create a custom PreferenceKey for the view size. Here is an example:

    struct ViewSizeKey: PreferenceKey {
        static var defaultValue: CGSize = .zero
    
        static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
            value = nextValue()
        }
    }
    

    Then, create a view which will calculate its size and assign it to the ViewSizeKey:

    struct ViewGeometry: View {
        var body: some View {
            GeometryReader { geometry in
                Color.clear
                    .preference(key: ViewSizeKey.self, value: geometry.size)
            }
        }
    }
    

    Now, you can use them in your ChildView (even if it's wrapped in a ScrollView):

    struct ChildView: View {
        @State var viewSize: CGSize = .zero
        
        var body: some View {
            HStack(spacing: 0) { // no spacing between HStack items
                Text("Left")
                    .frame(width: viewSize.width * (1 / 3))
                Text("Right")
                    .frame(width: viewSize.width * (2 / 3))
                    .background(Color.red.opacity(0.4))
            }
            .frame(minWidth: 0, maxWidth: .infinity)
            .background(Color.green.opacity(0.4))
            .background(ViewGeometry()) // calculate the view size
            .onPreferenceChange(ViewSizeKey.self) {
                viewSize = $0 // assign the size to `viewSize`
            }
                        
        }
    }