'SwiftUI VStack and Spacer not behaving according to docs
In a SwiftUI Apple Watch app, we need some text aligned in a vertical scroll view such that:
- if the content is small, it should be placed at the bottom of the screen.
- if the content does not fit into the lower half of the screen, it should extend off-screen at the bottom, such that the user has to scroll down to view the rest of the content.
Here's my take so far:
import SwiftUI
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
ZStack {
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 0) {
Spacer()
.frame(minHeight: geometry.size.height / 2)
Text("Title")
.bold()
.background(.red.opacity(0.2))
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
.background(.red.opacity(0.2))
}
.frame(minHeight: geometry.size.height - 20)
.background(.blue.opacity(0.4))
}
}
.padding(10)
.frame(width: geometry.size.width, height: geometry.size.height)
}
.ignoresSafeArea()
}
}
As you can see, the Spacer
and the frame(minHeight: ...)
part serve to place the content on the bottom. The ScrollView
's size is fixed to the whole screen.
However, the text is not displayed in full. Here's how it looks:
As you can see, the content does not start in the middle of the screen. Apparently the Spacer
gets larger than its minHeight
. However, the Spacer
is documented to only take excess space in a VStack
. There is not any excess space in that VStack
, so the Spacer
should only be as tall as its minHeight
.
What am I missing?
And here's how it looks when scrolled to the bottom:
Why is this text clipped?
Solution 1:[1]
The most important part to fix this is the layout priority. To prevent the Spacer
from infinitely expanding (because ScrollView
will give however much space its contents want) we need to define which takes priority. Without defining priority, by default SwiftUI appears to just evenly distribute the layout between the Spacer
and all the Text
s 50/50.
We can reduce the priority of the Spacer
expanding with the layoutPriority(_:)
modifier. A few other minor adjustments were made, such as setting a minimum height for the VStack
so the text aligns to the bottom when it is short.
Full code:
struct ContentView: View {
var body: some View {
GeometryReader { geo in
ScrollView {
VStack(alignment: .leading, spacing: 0) {
Spacer()
.frame(maxWidth: .infinity, minHeight: geo.size.height / 2)
.layoutPriority(-1)
Text("Title")
.bold()
.background(.red.opacity(0.2))
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
// Text("Much shorter text.")
.background(.red.opacity(0.2))
}
.frame(minHeight: geo.size.height)
.background(.blue.opacity(0.4))
}
}
.padding(10)
.navigationBarHidden(true)
.ignoresSafeArea()
}
}
Result:
Solution 2:[2]
You don't need spacer at all, instead alignment can be done by frame alignment, which is more appropriate in considered scenario
var body: some View {
GeometryReader { geometry in
ZStack {
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 0) {
Spacer()
.frame(height: geometry.size.height / 2) // << fixed !!
Text("Title")
.bold()
.background(.red.opacity(0.2))
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
.background(.red.opacity(0.2))
}
.frame(minHeight: geometry.size.height - 20, alignment: .bottom) // << !!
.background(.blue.opacity(0.4))
}
}
.padding(10)
}
.ignoresSafeArea()
}
Tested with Xcode 13.2
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 |