I'm new to Swift UI and I'm having troubles to make a specific UI behave the way I want.
I have a ScrollView
that has some content. This content in a VStack can be as small as 1-3 text components to more than 60 components.
I want the scroll view to grow as much as it needs. Once there's no remaining space just keep using the max available height and make use of the scroll.
The problem comes that even though I have 6 items, the ScrollView
takes the entire height and pushes the Text below. You can check the red background and the "Just right below the ScrollView" being pushed down.
struct ContentTestView: View {
var body: some View {
VStack {
// ScrollView should wrap content height...
ScrollView {
VStack(alignment: .leading) {
ForEach(0..<6) { index in
Text("Item \(index)")
.padding()
.background(Color.gray.opacity(0.3))
.cornerRadius(8)
.padding(.bottom, 4)
}
}
.frame(maxWidth: .infinity) // Fill max width
}
.background(.red)
Text("Just right below the ScrollView")
Spacer() // There should be a space if the scroll view content is not too big
Text("Bottom")
.padding()
.frame(maxWidth: .infinity) // Fill max width
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentTestView()
}
}
I've tried to use fixedSize()
but this doesn't work as I expect as it pushes the other VStack siblings and doesn't scroll the way I want.
What's the solution here? I want the ScrollView
to grow as much as it needs and once the content fills the entire height it should look like the previous image I showed you.
On android I could achieve by using .weight(1f, fill = false)
in the ScrollView
but here there's no similar approach.
As Benzy Neez said in the comments, you can use ViewThatFits
if you are on iOS 16+.
Write the content you want to display as the first view in ViewThatFits
, then write the same thing but wrapped in a ScrollView
, as the second view.
Here it is implemented as a ViewModifier
.
struct FixedSizeScrollView: ViewModifier {
let axis: Axis.Set
init(axis: Axis.Set) {
self.axis = axis
}
func body(content: Content) -> some View {
ViewThatFits(in: axis) {
content
ScrollView(axis) {
content
}
}
}
}
extension View {
func fixedSizeScrollView(_ axis: Axis.Set = .vertical) -> some View {
modifier(FixedSizeScrollView(axis: axis))
}
}
Usage:
VStack(alignment: .leading) {
ForEach(0..<6, id: \.self) { index in
Text("Item \(index)")
.padding()
.background(Color.gray.opacity(0.3))
.cornerRadius(8)
.padding(.bottom, 4)
}
}
.frame(maxWidth: .infinity)
.fixedSizeScrollView()
.background(.red)
If you want the content to be scrollable, even when it is small enough to fit, you can write a fixedSize
scroll view as the first view instead:
func body(content: Content) -> some View {
ViewThatFits(in: axis) {
ScrollView(axis) {
content
}
.fixedSize(
horizontal: axis.contains(.horizontal),
vertical: axis.contains(.vertical)
)
ScrollView(axis) {
content
}
}
}