iosswiftswiftui

Unexpected ViewThatFits Behavior: Why My ScrollView Isn’t Rendering for Large Content in SwiftUI


I want to display my content as-is, centered vertically, if it fits within the screen height.

I only want to wrap it in a ScrollView if its height exceeds the screen height.

I'm avoiding placing it in a ScrollView by default, because achieving vertical centering inside a ScrollView can be tricky.

I try to use ViewThatFits with ScrollView.

I use the following code to show small height content.


No ScrollView is rendered (Correct behaviour)

import SwiftUI

struct ContentView: View {
    var largeContent: some View {
        VStack {
            Spacer()
            
            ForEach(1...20, id: \.self) { i in
                Image(systemName: "globe")
                    .imageScale(.large)
                    .foregroundStyle(.tint)
                Text("\(i). Large content!")
                    .font(.largeTitle)
            }
            
            Spacer()
        }
    }
    
    var smallContent: some View {
        VStack {
            Spacer()
            
            ForEach(1...2, id: \.self) { i in
                Image(systemName: "globe")
                    .imageScale(.large)
                    .foregroundStyle(.tint)
                Text("\(i). Small content!")
                    .font(.largeTitle)
            }
            
            Spacer()
        }
    }
    
    var body: some View {
        VStack {
            let smallContent = smallContent
            
            // The top portion dedicated to content
            GeometryReader { x in
                ViewThatFits {
                    // Option 1: Content fits without scrolling.
                    smallContent
                        .background(Color.yellow)
                        .frame(maxHeight: x.size.height)
                        .frame(maxWidth: .infinity)
                    
                    // Option 2: Content is too tall, so wrap it in a ScrollView.
                    ScrollView {
                        smallContent
                            .background(Color.red)
                            .frame(minHeight: x.size.height)
                            .frame(maxWidth: .infinity)
                    }
                }
            }
            
            Text("Button")
                .background(Color.blue)
                .font(.largeTitle)
        }
    }
}

enter image description here

It is within my expectation because

  1. No ScrollView is rendered.
  2. Content is vertical center.

No ScrollView is rendered (Wrong behaviour)

import SwiftUI

struct ContentView: View {
    var largeContent: some View {
        VStack {
            Spacer()
            
            ForEach(1...20, id: \.self) { i in
                Image(systemName: "globe")
                    .imageScale(.large)
                    .foregroundStyle(.tint)
                Text("\(i). Large content!")
                    .font(.largeTitle)
            }
            
            Spacer()
        }
    }
    
    var smallContent: some View {
        VStack {
            Spacer()
            
            ForEach(1...2, id: \.self) { i in
                Image(systemName: "globe")
                    .imageScale(.large)
                    .foregroundStyle(.tint)
                Text("\(i). Small content!")
                    .font(.largeTitle)
            }
            
            Spacer()
        }
    }
    
    var body: some View {
        VStack {
            let largeContent = largeContent
            
            // The top portion dedicated to content
            GeometryReader { x in
                ViewThatFits {
                    // Option 1: Content fits without scrolling.
                    largeContent
                        .background(Color.yellow)
                        .frame(maxHeight: x.size.height)
                        .frame(maxWidth: .infinity)
                    
                    // Option 2: Content is too tall, so wrap it in a ScrollView.
                    ScrollView {
                        largeContent
                            .background(Color.red)
                            .frame(minHeight: x.size.height)
                            .frame(maxWidth: .infinity)
                    }
                }
            }
            
            Text("Button")
                .background(Color.blue)
                .font(.largeTitle)
        }
    }
}

#Preview {
    ContentView()
}

enter image description here

Now, I have switch to display "large content". I am expecting red ScrollView to be rendered. However, it is not.

May I know what's wrong with my code? Thank you.


Solution

  • The reason why the yellow version is shown when largeContent is shown, is because you are enforcing a maximum height equal to the GeometryReader height. This fits! So this is the view that's shown.

    What then happens, is that the view overflows.

    You can see the consequences better if you change the order of modifiers, so that .background follows .frame. Then try applying .clipped:

    ViewThatFits {
        // Option 1: Content fits without scrolling.
        largeContent
            .frame(maxHeight: x.size.height)
            .frame(maxWidth: .infinity)
            .background(Color.yellow)
            .clipped()
    
        // Option 2: Content is too tall, so wrap it in a ScrollView.
        ScrollView {
            // ...
        }
    }
    

    Screenshot

    If you replace x.size.height with .infinity, so that it can use all the height it actually needs, it works as you are expecting:

    largeContent
        .frame(maxHeight: .infinity) // 👈 here
        .frame(maxWidth: .infinity)
        .background(Color.yellow)
        .clipped()
    

    Screenshot