swiftswiftuiswiftui-scrollview

SwiftUI: Making elements of a ScrollView match the height of the biggest element


I have a horizontal ScrollView with dot page control (based on this answer SwiftUI create image slider with dots as indicators), and every element in it is a custom view. The height of every element is determined by the data I pass into it, it can have two or three vertical sections depending on some conditions.

At the moment it looks something like this:

Existing ScrollView

I want all the elements in my ScrollView to match the height of the biggest element, but I can't come up with a way of doing it, because we need all the elements to have been rendered in order to find out the height of the biggest one, but once they have been rendered, I can't change the height of the elements.

This is what I want it to look like, I want the bottom section to stretch to match the height of the biggest element (although I'd be fine with it resizing any other way, but I need it to match the biggest element):

Desired ScrollView

I am very inexperienced in SwiftUI and am probably missing something.

Here is some of the code:

struct DotIndicatorCollectionView: View {
    var items: [CustomItem]
    @State private var index = 0

    var body: some View {
        return PagingView(index: $index.animation(), maxIndex: items.count - 1) {
            ForEach(items, id: \.self) { item in
                 CustomView(
                         item: item
                     )
                     .padding(.horizontal, 20)

                )
            }
                    }
    }
}

PagingView code is provided here: SwiftUI create image slider with dots as indicators


Solution

  • One way to solve this is to establish the height of the tallest item in a hidden view, then show the PagingView as an overlay.

    Here is an example to show it working. It uses dummy implementations of the structs and views that you had in your original code:

    struct CustomItem: Identifiable {
        let id = UUID()
        let title: String
        let text: String
    }
    
    struct CustomView: View {
        let item: CustomItem
    
        var body: some View {
            VStack(spacing: 20) {
                Text(item.title)
                    .font(.largeTitle)
                Text(item.text)
            }
            .padding()
        }
    }
    
    struct DotIndicatorCollectionView: View {
        var items: [CustomItem] = [
            CustomItem(title: "Item 1", text: "One line of text"),
            CustomItem(title: "Item 2", text: "Two lines of text\nTwo lines of text"),
            CustomItem(title: "Item 3", text: "Three lines of text\nThree lines of text\nThree lines of text")
        ]
    
        @State private var index = 0
    
        @ViewBuilder
        private func theItems(maxHeight: CGFloat?) -> some View {
            ForEach(items) { item in
                CustomView(
                    item: item
                )
                .padding(.horizontal, 20)
                .frame(maxHeight: maxHeight)
                .background(.orange)
            }
        }
    
        var body: some View {
            ZStack {
                theItems(maxHeight: nil)
            }
            .frame(maxWidth: .infinity)
            .hidden()
            .overlay {
                PagingView(index: $index.animation(), maxIndex: items.count - 1) {
                    theItems(maxHeight: .infinity)
                }
            }
        }
    }
    

    You will notice in this example that the orange background is applied after setting maxHeight: .infinity on a CustomView. If you do it the other way around then the views will have the maximum height, but the background behind them won't actually fill the height.

    If your real CustomView includes a background color or rounded corners or some other decoration that depends on the height, then you have two choices: