Search code examples
iosswiftuimobile

Force views to have the same dimensions based on the biggest one


I want to have a grid of buttons. The buttons will take as much horizontal space as available and will divide it uniformly. Additionally, the height of all the buttons should be the same based on the tallest one. Somehow I am not able to make these requirements to work together. Here for example, the height of each row is the same, but the heights between rows are different.

struct ContentView: View {
    let texts = [["Short", "A bit longer text", "bbb"],
                 ["a", "A bit longer text", "This is a very long text that might not fit in one line"],
                 ["a", "b", "c"]
                 ]
    
    var body: some View {
        VStack {
            ForEach(texts, id: \.self) { row in
                HStack {
                    ForEach(row, id: \.self) { text in
                        Button{ } label: {
                            Text(text)
                                .foregroundStyle(.white)
                                .padding()
                                .frame(maxWidth: .infinity, maxHeight: .infinity)
                                .background(.red)
                                .clipShape(Capsule())
                        }
                    }
                }
                .fixedSize(horizontal: false, vertical: true)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            }
        }
    }
}

#Preview {
    ContentView()
}

I tried experimenting more, but nothing works and I have no intuition on how to progressively make the UI look more as I intend it to be. Every small change breaks everything in a big way.


Solution

  • One way to solve this is to establish the size of the footprint for the largest text label, then show the label of each button as an overlay over a (hidden) footprint.

    The footprint can be found using a ZStack with all the strings layered on top of each other. The size of the ZStack is determined by the size of the largest text string:

    private var footprint: some View {
        ZStack {
            ForEach(Array(texts.enumerated()), id: \.offset) { offset, row in
                ForEach(Array(row.enumerated()), id: \.offset) { offset, text in
                    Text(text)
                }
            }
        }
        .hidden()
    }
    

    After that, you can use a VStack in combination with an HStack to put the grid together, like you were doing before, or you can just use a Grid:

    var body: some View {
        Grid {
            ForEach(Array(texts.enumerated()), id: \.offset) { offset, row in
                GridRow {
                    ForEach(Array(row.enumerated()), id: \.offset) { offset, text in
                        Button {} label: {
                            footprint
                                .overlay { Text(text) }
                                .padding()
                                .foregroundStyle(.white)
                                .background {
                                    Capsule().fill(.red)
                                }
                        }
                    }
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            }
        }
    }
    

    Screenshot

    If you don't want the buttons to be so spread out, just remove the .frame at the end.