Reputation: 5220
I'm trying to have the content inside a ScrollView
be centered when that content is small enough to not require scrolling, but instead it aligns to the top. Is this a bug or I'm missing adding something? Using Xcode 11.4 (11E146)
@State private var count : Int = 100
var body : some View {
// VStack {
ScrollView {
VStack {
Button(action: {
if self.count > 99 {
self.count = 5
} else {
self.count = 100
}
}) {
Text("CLICK")
}
ForEach(0...count, id: \.self) { no in
Text("entry: \(no)")
}
}
.padding(8)
.border(Color.red)
.frame(alignment: .center)
}
.border(Color.blue)
.padding(8)
// }
}
Upvotes: 9
Views: 3149
Reputation: 257493
You observe just normal ScrollView behaviour. Here is a demo of possible approach to achieve your goal.
// view pref to detect internal content height
struct ViewHeightKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
// extension for modifier to detect view height
extension ViewHeightKey: ViewModifier {
func body(content: Content) -> some View {
return content.background(GeometryReader { proxy in
Color.clear.preference(key: Self.self, value: proxy.size.height)
})
}
}
// Modified your view for demo
struct TestAdjustedScrollView: View {
@State private var count : Int = 100
@State private var myHeight: CGFloat? = nil
var body : some View {
GeometryReader { gp in
ScrollView {
VStack {
Button(action: {
if self.count > 99 {
self.count = 5
} else {
self.count = 100
}
}) {
Text("CLICK")
}
ForEach(0...self.count, id: \.self) { no in
Text("entry: \(no)")
}
}
.padding(8)
.border(Color.red)
.frame(alignment: .center)
.modifier(ViewHeightKey()) // read content view height !!
}
.onPreferenceChange(ViewHeightKey.self) {
// handle content view height
self.myHeight = $0 < gp.size.height ? $0 : gp.size.height
}
.frame(height: self.myHeight) // align own height with content
.border(Color.blue)
.padding(8)
}
}
}
Upvotes: 6
Reputation: 58019
For me, GeometryReader aligned things to the top no matter what. I solved it with adding two extra Spacers (my code is based on this answer):
GeometryReader { metrics in
ScrollView {
VStack(spacing: 0) {
Spacer()
// your content goes here
Spacer()
}
.frame(minHeight: metrics.size.height)
}
}
Upvotes: 0
Reputation: 189
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.
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.
@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)
}
}
@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
.
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.ScrollView
, ScrollViewContentContainer
pushes it to the top or leading edge.Here's why the content gets centered.
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..frame(minWidth:,minHeight:)
method on the ScrollView
content ensures that it is never smaller than the ScrollView
.VStack
or HStack
allows the content to be centered.ScrollView
if needed, and ScrollViewContentContainer
retains its default behavior of aligning to the top or leading edge.Upvotes: 11
Reputation: 76
The frame(alignment: .center)
modifier you’ve added doesn’t work since what it does is wrapping your view in a new view of exactly the same size. Because of that the alignment doesn’t do anything as there is no additional room for the view do be repositioned.
One potential solution for your problem would be to wrap the whole ScrollView
in a GeometryReader
to read available height. Then use that height to specify that the children should not be smaller than it. This will then make your view centered inside of ScrollView
.
struct ContentView: View {
@State private var count : Int = 100
var body : some View {
GeometryReader { geometry in
ScrollView {
VStack {
Button(action: {
if self.count > 99 {
self.count = 5
} else {
self.count = 100
}
}) {
Text("CLICK")
}
ForEach(0...self.count, id: \.self) { no in
Text("entry: \(no)")
}
}
.padding(8)
.border(Color.red)
.frame(minHeight: geometry.size.height) // Here we are setting minimum height for the content
}
.border(Color.blue)
}
}
}
Upvotes: 6