I'm having an UI contains an expandable ScrollView
struct ContentView: View {
var body: some View {
VStack(spacing: 8) {
Spacer()
ScrollView {
ExpandView()
}
.fixedSize(horizontal: false, vertical: true)
Button {
} label: {
HStack {
Spacer()
Text("Add")
Spacer()
}
.background(Color.green)
}
}
.frame(maxWidth: .infinity)
}
}
struct ExpandView: View {
var body: some View {
VStack {
Accordion(title: "Red", content: {
Rectangle()
.fill(.red)
.frame(height: 600)
})
Accordion(title: "Blue", content: {
Rectangle()
.fill(.blue)
.frame(height: 600)
})
}
}
}
struct Accordion<Content: View>: View {
@State var isExpand: Bool = false
let title: String
@ViewBuilder let content: Content
var body: some View {
VStack(spacing: 0) {
Button(action: {
withAnimation {
isExpand.toggle()
}
}, label: {
HStack {
Spacer()
Text(title)
Spacer()
}
})
.buttonStyle(.plain)
.frame(height: 44)
.background(Color.yellow)
if isExpand {
content
.padding(.horizontal, 12)
.transition(.move(edge: .top))
}
}
.clipped()
}
}
Where user tap on Yellow Button, ScrollView
will expand base on it content size. It works well if content size of ScrollView
is small, but when it become larger. Button Add
get pushed out of screen and can not scroll anymore.
I have try to add frame(maxHeight: )
modifer but problem still occurs. I'm expect ScrollView's frame only expand upto top safearea and not push out Button Add
Here is a gif of current bug: https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExNzkxeTYxYnFicHlhcWF4d3R6aXZpdzN0MjUzYWdhbW0wODdhNnQ3ZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/9D7qzWbgw79V4bY3TP/giphy.gif
It is almost always incorrect to put fixedSize
directly on a ScrollView
, because the whole point of a ScrollView
is to allow its contents to overflow its own bounds so that it can be scrolled, not to have the scroll view's frame be exactly the same as its contents.
If you want the scroll view's contents to start at the bottom, you can use defaultScrollAnchor
:
VStack(spacing: 8) {
ScrollView {
ExpandView()
}
// 'for: .alignment' can be added in iOS 18+
.defaultScrollAnchor(.bottom/*, for: .alignment*/)
// ...
}
Alternatively, use a GeometryReader
and set the frame(maxHeight:)
, then add another frame
that positions the scroll view at the bottom of the GeometryReader
, akin to what your Spacer
did. Spacer
wouldn't work here because GeometryReader
is more "greedy" than Spacer
and will take up all the space instead.
VStack(spacing: 8) {
GeometryReader { geo in
ScrollView {
ExpandView()
}
.frame(maxHeight: geo.size.height)
// here I set the max height before fixSize, so there is a limit as to how tall the scroll view can expand
.fixedSize(horizontal: false, vertical: true)
.frame(height: geo.size.height, alignment: .bottom)
}
}