swiftuiswiftui-scrollview

ScrollView takes the entire height when content is small


I'm new to Swift UI and I'm having troubles to make a specific UI behave the way I want.

I have a ScrollView that has some content. This content in a VStack can be as small as 1-3 text components to more than 60 components.

I want the scroll view to grow as much as it needs. Once there's no remaining space just keep using the max available height and make use of the scroll.

The problem comes that even though I have 6 items, the ScrollView takes the entire height and pushes the Text below. You can check the red background and the "Just right below the ScrollView" being pushed down. enter image description here

struct ContentTestView: View {
    var body: some View {
        VStack {
            // ScrollView should wrap content height...
            ScrollView {
                VStack(alignment: .leading) {
                    ForEach(0..<6) { index in
                        Text("Item \(index)")
                            .padding()
                            .background(Color.gray.opacity(0.3))
                            .cornerRadius(8)
                            .padding(.bottom, 4)
                    }
                }
                .frame(maxWidth: .infinity) // Fill max width
            }
            .background(.red)
            Text("Just right below the ScrollView")
            
            
            Spacer() // There should be a space if the scroll view content is not too big
            
            Text("Bottom")
                .padding()
                .frame(maxWidth: .infinity) // Fill max width
                .background(Color.blue)
                .foregroundColor(.white)
                .cornerRadius(8)
        }
        .padding()
    }
}

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

I've tried to use fixedSize() but this doesn't work as I expect as it pushes the other VStack siblings and doesn't scroll the way I want.

What's the solution here? I want the ScrollView to grow as much as it needs and once the content fills the entire height it should look like the previous image I showed you.

On android I could achieve by using .weight(1f, fill = false) in the ScrollView but here there's no similar approach.


Solution

  • As Benzy Neez said in the comments, you can use ViewThatFits if you are on iOS 16+.

    Write the content you want to display as the first view in ViewThatFits, then write the same thing but wrapped in a ScrollView, as the second view.

    Here it is implemented as a ViewModifier.

    struct FixedSizeScrollView: ViewModifier {
        let axis: Axis.Set
        
        init(axis: Axis.Set) {
            self.axis = axis
        }
        
        func body(content: Content) -> some View {
            ViewThatFits(in: axis) {
                content
                ScrollView(axis) {
                    content
                }
            }
        }
    }
    
    extension View {
        func fixedSizeScrollView(_ axis: Axis.Set = .vertical) -> some View {
            modifier(FixedSizeScrollView(axis: axis))
        }
    }
    

    Usage:

    VStack(alignment: .leading) {
        ForEach(0..<6, id: \.self) { index in
            Text("Item \(index)")
                .padding()
                .background(Color.gray.opacity(0.3))
                .cornerRadius(8)
                .padding(.bottom, 4)
        }
    }
    .frame(maxWidth: .infinity)
    .fixedSizeScrollView()
    .background(.red)
    

    If you want the content to be scrollable, even when it is small enough to fit, you can write a fixedSize scroll view as the first view instead:

    func body(content: Content) -> some View {
        ViewThatFits(in: axis) {
            ScrollView(axis) {
                content
            }
            .fixedSize(
                horizontal: axis.contains(.horizontal), 
                vertical: axis.contains(.vertical)
            )
            ScrollView(axis) {
                content
            }
        }
    }