You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现可折叠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()
}

核心原理

  1. 捕获滚动偏移:通过PreferenceKey和GeometryReader实时获取ScrollView的滚动偏移量,仅在展开状态下保存有效数据。
  2. 限制偏移范围:利用内容总高度计算最大可滚动偏移,避免恢复时超出内容边界。
  3. 平滑恢复位置:使用ScrollViewReader结合顶部锚点,通过scrollBy精准恢复滚动位置,同时用withAnimation(.none)避免与折叠/展开动画冲突。

低iOS版本兼容提示

如果需要支持iOS 17以下版本(scrollBy为iOS 17新增API),可以改为通过偏移量计算对应item的索引,再用proxy.scrollTo(items[index], anchor: .top)恢复位置,前提是需要提前获取每个item的高度并累加计算。

内容的提问来源于stack exchange,提问作者Vladimir Nikitin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 18:56:04