I expect following code to show a scroll view that collapses/expands on button press. However after the expansion scroll position is not preserved. Is there any way to fix this?
import SwiftUI
struct CollapsibleScrollView: View {
var items = (0..<100).map { "\($0)" }
@State var isCollapsed: Bool = false
var body: some View {
ZStack {
VStack {
Button {
withAnimation {
isCollapsed.toggle()
}
} label: {
Text(isCollapsed ? "Expand" : "Collapse")
}
Spacer()
}
if !isCollapsed {
scrollView
}
}
}
private var scrollView: some View {
ScrollView {
VStack {
ForEach(items, id: \.self) { item in
GroupBox {
HStack {
Text(item)
}
.frame(maxWidth: .infinity)
}
}
}
}
.frame(maxHeight: 200)
}
}
#Preview {
CollapsibleScrollView()
}
P.S. "0 height" solution works, but it doesn't work for me since I really need scroll view to be inside if-clause for animation purposes.
P.P.S. Best I could come up with is keeping track of current scroll position with .scrollPosition
modifier and reset the scroll after expansion, but it looks ugly.
I expect following code to show a scroll view that collapses/expands on button press. However after the expansion scroll position is not preserved. Is there any way to fix this?
import SwiftUI
struct CollapsibleScrollView: View {
var items = (0..<100).map { "\($0)" }
@State var isCollapsed: Bool = false
var body: some View {
ZStack {
VStack {
Button {
withAnimation {
isCollapsed.toggle()
}
} label: {
Text(isCollapsed ? "Expand" : "Collapse")
}
Spacer()
}
if !isCollapsed {
scrollView
}
}
}
private var scrollView: some View {
ScrollView {
VStack {
ForEach(items, id: \.self) { item in
GroupBox {
HStack {
Text(item)
}
.frame(maxWidth: .infinity)
}
}
}
}
.frame(maxHeight: 200)
}
}
#Preview {
CollapsibleScrollView()
}
P.S. "0 height" solution works, but it doesn't work for me since I really need scroll view to be inside if-clause for animation purposes.
P.P.S. Best I could come up with is keeping track of current scroll position with .scrollPosition
modifier and reset the scroll after expansion, but it looks ugly.
1 Answer
Reset to default 1The easiest way to retain the scroll position is to keep the ScrollView
present in the layout, instead of using an if
block to remove it when collapsed. Then use maxHeight
to determine whether to show it collapsed or expanded:
- to collapse the scroll view, set
maxHeight: 0
- to show it expanded, set
maxHeight: nil
.
Here is the updated example to show it working this way. Other concerns are discussed below:
var body: some View {
VStack {
Button {
withAnimation(.easeInOut) {
isCollapsed.toggle()
}
} label: {
Text(isCollapsed ? "Expand" : "Collapse")
}
scrollView
.frame(maxHeight: isCollapsed ? 0 : nil)
.padding(.top, 100)
}
.padding(.vertical, 100)
.frame(maxHeight: .infinity, alignment: .top)
}
// private var scrollView: as before
In the question you were saying:
I really need scroll view to be inside if-clause for animation purposes
If this is the case, you will need to track the scroll position, so that it can be restored when the scroll view is made visible again. Using .scrollPosition
would be a way of doing this, but you said you didn't like this either. So you might have to make a compromise somewhere.
I would have thought, a transition-like animation would be possible, without having to remove the ScrollView
from the layout completely. For example, a sliding horizontal movement can be achieved by adjusting the x-offset. You might also want to chain certain animations together. Using withAnimation
combined with a completion
callback is one technique that can be used to do this.
.transition(insertion: .move(edge: .leading), removal: .move(edge: .leading))
modifier to scrollView in order to perform" slide behind the screen" effect. In fact there is quite a lot this scroll view has to do, but I assume it's irrelevant to the problem. 2. By ugly I mean both. The effect itself turns out to be scroll view expanding with first element scrolled to and then it scrolls to the tracked position, I need it to be static for my purposes. The fact that I have to add this scroll position tracking code looks ugly too. – Vladimir Nikitin Commented Mar 18 at 22:26