Credit goes to @Thaniel for finding the solution. My intention here is to more fully explain what is happening behind the scenes to demystify SwiftUI and explain why the solution works.
Solution
Wrap the ScrollView
inside a GeometryReader
so that you can set the minimum height (or width if the scroll view is horizontal) of the scrollable content to match the height of the ScrollView
. This will make it so that the dimensions of the scrollable area are never smaller than the dimensions of the ScrollView
. You can also declare a static dimension and use it to set the height of both the ScrollView
and its content.
Dynamic Height
@State private var count : Int = 5
var body: some View {
// use GeometryReader to dynamically get the ScrollView height
GeometryReader { geometry in
ScrollView {
VStack(alignment: .leading) {
ForEach(0...self.count, id: \.self) { num in
Text("entry: \(num)")
}
}
.padding(10)
// border is drawn before the height is changed
.border(Color.red)
// match the content height with the ScrollView height and let the VStack center the content
.frame(minHeight: geometry.size.height)
}
.border(Color.blue)
}
}
Static Height
@State private var count : Int = 5
// set a static height
private let scrollViewHeight: CGFloat = 800
var body: some View {
ScrollView {
VStack(alignment: .leading) {
ForEach(0...self.count, id: \.self) { num in
Text("entry: \(num)")
}
}
.padding(10)
// border is drawn before the height is changed
.border(Color.red)
// match the content height with the ScrollView height and let the VStack center the content
.frame(minHeight: scrollViewHeight)
}
.border(Color.blue)
}
The bounds of the content appear to be smaller than the ScrollView
as shown by the red border. This happens because the frame is set after the border is drawn. It also illustrates the fact that the default size of the content is smaller than the ScrollView
.
Why Does it Work?
ScrollView
First, let's understand how SwiftUI's ScrollView
works.
ScrollView
wraps it's content in a child element called ScrollViewContentContainer
.
ScrollViewContentContainer
is always aligned to the top or leading edge of the ScrollView
depending on whether it is scrollable along the vertical or horizontal axis or both.
ScrollViewContentContainer
sizes itself according to the ScrollView
content.
- When the content is smaller than the
ScrollView
, ScrollViewContentContainer
pushes it to the top or leading edge.
Center Align
Here's why the content gets centered.
- The solution relies on forcing the
ScrollViewContentContainer
to have the same width and height as its parent ScrollView
.
GeometryReader
can be used to dynamically get the height of the ScrollView
or a static dimension can be declared so that both the ScrollView
and its content can use the same parameter to set their horizontal or vertical dimension.
- Using the
.frame(minWidth:,minHeight:)
method on the ScrollView
content ensures that it is never smaller than the ScrollView
.
- Using a
VStack
or HStack
allows the content to be centered.
- Because only the minimum height is set, the content can still grow larger than the
ScrollView
if needed, and ScrollViewContentContainer
retains its default behavior of aligning to the top or leading edge.