I have content within an HStack that is narrower than the available space and use Spacers to fill the entire width. However, because of dynamic font size, the content can get too large, and a ScrollView is required instead of the Spacers. But Spacers don't work within Scrollviews.
An example with simple Text. The Text should neither be wrapped nor abbreviated.
import SwiftUI
struct ScrollViewSandbox: View {
var body: some View {
// ScrollView(.horizontal) {
HStack {
Text("Hello, World!")
Spacer()
Text("Hello,")
Spacer()
Text("World!")
}.lineLimit(1)
// }
}
}
#Preview {
ScrollViewSandbox()
}
At default font size, everything is fine:
At some point, a ScrollView is required instead of Spacers as the content is too wide:
A ScrollView makes the Text readable again, but then Spacers don't work anymore, and everything is left aligned:
I can use GeometryReader to set HStack minWidth and solve the issue above. However, this causes the content of GeometryReader to shrink to the wrong height when used within a (vertical) ScrollView, causing overlapping.
Example
import SwiftUI
struct ScrollViewSandbox: View {
var body: some View {
ScrollView {
GeometryReader { prox in
ScrollView(.horizontal) {
HStack(spacing: 20) {
VStack {
Text("Line 1")
Text("Line 2")
}
Spacer()
VStack {
Text("Line 1")
Text("Line 2")
}
Spacer()
VStack {
Text("Line 1")
Text("Line 2")
}
}.frame(minWidth: prox.size.width)
}
}
Text("Another line")
}
}
}
#Preview {
ScrollViewSandbox()
}
So overall, I want to use the Spacers as long as the content is narrower than the available space and use the ScrollView if the content is larger than the available space. How can I archive that behavior?
The original problem with the horizontal ScrollView
was solved by nesting it inside a GeometryReader
, as per the answer to How to make HStack fill its parent view's width (Scroll View) (it was my answer).
The new problem is that the height of the GeometryReader
is wrong. Normally, a GeometryReader
is greedy and uses all the space available, but because it is nested inside another ScrollView
(this time, one that scrolls vertically), it shrinks to a minimum. This is why the last Text
is seen to overlap the content of the HStack
.
Here are two workarounds:
1. Move the outer ScrollView
inside the GeometryReader
If you nest the outer ScrollView
inside the GeometryReader
, instead of vice versa, then the GeometryReader
occupies all of the available space in the usual way.
However, two additional changes are required with this approach:
VStack
is required, to define the layout for the views inside the GeometryReader
ScrollView
to adopt its ideal height, instead of its maximum height, .fixedSize(horizontal: false, vertical: true)
can be applied.var body: some View {
GeometryReader { prox in
VStack(spacing: 0) {
ScrollView {
ScrollView(.horizontal) {
HStack(spacing: 20) {
VStack {
Text("Line 1")
Text("Line 2")
}
Spacer()
VStack {
Text("Line 1")
Text("Line 2")
}
Spacer()
VStack {
Text("Line 1")
Text("Line 2")
}
}
.frame(minWidth: prox.size.width)
}
.background(.yellow)
}
.fixedSize(horizontal: false, vertical: true)
Text("Another line")
}
}
}
2. Show the nested GeometryReader
in an overlay
If the original hierarchy is used, where a ScrollView
is the top-level container, then the problem with the incorrect height for the GeometryReader
can be solved by establishing the footprint for the HStack
using hidden content. The GeometryReader
can then be shown in an overlay over this hidden content.
struct ScrollViewSandbox: View {
private var footprint: some View {
VStack {
Text(".")
Text(".")
}
.frame(maxWidth: .infinity)
.hidden()
}
var body: some View {
ScrollView {
footprint
.background(.yellow)
.overlay {
GeometryReader { prox in
ScrollView(.horizontal) {
HStack(spacing: 20) {
VStack {
Text("Line 1")
Text("Line 2")
}
Spacer()
VStack {
Text("Line 1")
Text("Line 2")
}
Spacer()
VStack {
Text("Line 1")
Text("Line 2")
}
}
.frame(minWidth: prox.size.width)
}
}
}
Text("Another line")
}
}
}
The result is the same as before.
For the example here, the footprint just consists of two lines of dummy text. This is sufficient for determining the height of the main content. If the main content is not so predictable, then you could always build the footprint by using a copy of the main content.