如何实现可折叠ScrollView并保留滚动位置?
解决方案
因为将ScrollView放在if语句内会导致折叠时视图被销毁,滚动状态也随之丢失,我们可以通过手动捕获并恢复滚动偏移量的方式解决这个问题,同时满足你的动画需求。以下是优化后的代码:
import SwiftUI // 用于捕获ScrollView滚动偏移量的PreferenceKey struct ScrollOffsetPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } } struct CollapsibleScrollView: View { var items = (0..<100).map { "\($0)" } @State var isCollapsed: Bool = false // 保存滚动偏移量 @State private var savedScrollOffset: CGFloat = 0 // 保存内容总高度,用于限制最大偏移范围 @State private var contentTotalHeight: CGFloat = 0 var body: some View { ZStack { VStack { Button { withAnimation { isCollapsed.toggle() } } label: { Text(isCollapsed ? "Expand" : "Collapse") } Spacer() } if !isCollapsed { scrollView } } } private var scrollView: some View { ScrollViewReader { proxy in ScrollView { VStack(spacing: 0) { // 顶部隐藏锚点,用于计算滚动基准位置 Color.clear .frame(height: 0) .id("scrollAnchor") ForEach(items, id: \.self) { item in GroupBox { HStack { Text(item) } .frame(maxWidth: .infinity) } } } .background( GeometryReader { contentGeo in Color.clear // 捕获滚动偏移:内容视图在scrollView坐标系中的y原点负值即为滚动偏移 .preference(key: ScrollOffsetPreferenceKey.self, value: -contentGeo.frame(in: .named("scrollView")).origin.y) .onAppear { // 保存内容总高度 contentTotalHeight = contentGeo.size.height } } ) } .coordinateSpace(name: "scrollView") .frame(maxHeight: 200) .onPreferenceChange(ScrollOffsetPreferenceKey.self) { newOffset in // 仅在展开状态下保存有效偏移,避免超出内容范围 if !isCollapsed { savedScrollOffset = min(newOffset, contentTotalHeight - 200) } } .onAppear { // 异步恢复滚动位置,避免与折叠/展开动画冲突 DispatchQueue.main.async { withAnimation(.none) { // 先定位到顶部锚点,再通过scrollBy恢复偏移 proxy.scrollTo("scrollAnchor", anchor: .top) proxy.scrollBy(x: 0, y: -savedScrollOffset) } } } } } } #Preview { CollapsibleScrollView() }
核心原理
- 捕获滚动偏移:通过
PreferenceKey和GeometryReader实时获取ScrollView的滚动偏移量,仅在展开状态下保存有效数据。 - 限制偏移范围:利用内容总高度计算最大可滚动偏移,避免恢复时超出内容边界。
- 平滑恢复位置:使用
ScrollViewReader结合顶部锚点,通过scrollBy精准恢复滚动位置,同时用withAnimation(.none)避免与折叠/展开动画冲突。
低iOS版本兼容提示
如果需要支持iOS 17以下版本(scrollBy为iOS 17新增API),可以改为通过偏移量计算对应item的索引,再用proxy.scrollTo(items[index], anchor: .top)恢复位置,前提是需要提前获取每个item的高度并累加计算。
内容的提问来源于stack exchange,提问作者Vladimir Nikitin
相关产品推荐
相关产品推荐

