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.
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)
}
}
}
It is within my expectation because
ScrollView
is rendered.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()
}
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.
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 {
// ...
}
}
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()