SwiftUI中关闭视图后如何保留ScrollView的滚动位置?
问题:ScrollView返回后回到起始位置,如何保留选中项的滚动位置
我有一个水平ScrollView,遍历数字数组生成红色卡片。点击卡片时,卡片会展开单独显示。但关闭展开视图返回原状态时,整个视图会重新渲染,导致ScrollView回到起始位置(附示例动图)。希望返回后能保留选中元素的位置,不让ScrollView回到开头。

struct TestView: View { @State private var items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] @State private var isAnimationActive = false @State private var selectedItem = "" @Namespace private var animation var body: some View { ZStack { if !isAnimationActive { ScrollView(.horizontal, showsIndicators: false) { HStack { ForEach(items, id: \.self) { item in ZStack { RoundedRectangle(cornerRadius: 30, style: .continuous) .fill(Color.red) .matchedGeometryEffect(id: "card\(item.description)", in: animation) .frame(width: 250, height: 250) if selectedItem == item.description && isAnimationActive { Rectangle() .fill(.green) } else { Text(item.description) .foregroundColor(.white) .matchedGeometryEffect(id: "text\(item.description)", in: animation) } } .onTapGesture { withAnimation(.easeOut(duration: 0.3)) { isAnimationActive = true selectedItem = item.description } } } } .padding(.leading, 20) } } if isAnimationActive { ZStack { RoundedRectangle(cornerRadius: 30, style: .continuous) .fill(Color.red) .matchedGeometryEffect(id: "card\(selectedItem.description)", in: animation) .frame(width: 300, height: 400) Text(selectedItem) .foregroundColor(.white) .matchedGeometryEffect(id: "text\(selectedItem.description)", in: animation) } .transition(.offset(x: 1, y: 1)) .onTapGesture { withAnimation(.easeOut(duration: 0.3)) { isAnimationActive = false } } } } } }
解决方案
问题根源是isAnimationActive切换时,ScrollView被完全移除再重建,导致滚动位置丢失。以下两种方法可修复:
方法1:用透明度隐藏而非移除ScrollView
保留ScrollView的存在,通过透明度控制显隐,这样ScrollView的滚动状态会被完整保留:
struct TestView: View { @State private var items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] @State private var isAnimationActive = false @State private var selectedItem = "" @Namespace private var animation var body: some View { ZStack { // 保留ScrollView,用opacity控制显隐 ScrollView(.horizontal, showsIndicators: false) { HStack { ForEach(items, id: \.self) { item in ZStack { RoundedRectangle(cornerRadius: 30, style: .continuous) .fill(Color.red) .matchedGeometryEffect(id: "card\(item.description)", in: animation) .frame(width: 250, height: 250) if selectedItem == item.description && isAnimationActive { Rectangle() .fill(.green) } else { Text(item.description) .foregroundColor(.white) .matchedGeometryEffect(id: "text\(item.description)", in: animation) } } .onTapGesture { withAnimation(.easeOut(duration: 0.3)) { isAnimationActive = true selectedItem = item.description } } } } .padding(.leading, 20) } .opacity(isAnimationActive ? 0 : 1) // 核心修改:用透明度替代条件移除 if isAnimationActive { ZStack { RoundedRectangle(cornerRadius: 30, style: .continuous) .fill(Color.red) .matchedGeometryEffect(id: "card\(selectedItem.description)", in: animation) .frame(width: 300, height: 400) Text(selectedItem) .foregroundColor(.white) .matchedGeometryEffect(id: "text\(selectedItem.description)", in: animation) } .transition(.offset(x: 1, y: 1)) .onTapGesture { withAnimation(.easeOut(duration: 0.3)) { isAnimationActive = false } } } } } }
方法2:手动记录并恢复滚动位置
如果必须移除ScrollView,可借助ScrollViewReader记录选中项的位置,返回时自动滚动到目标位置:
struct TestView: View { @State private var items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] @State private var isAnimationActive = false @State private var selectedItem = "" @Namespace private var animation var body: some View { ZStack { if !isAnimationActive { ScrollViewReader { proxy in ScrollView(.horizontal, showsIndicators: false) { HStack { ForEach(items, id: \.self) { item in ZStack { RoundedRectangle(cornerRadius: 30, style: .continuous) .fill(Color.red) .matchedGeometryEffect(id: "card\(item.description)", in: animation) .frame(width: 250, height: 250) if selectedItem == item.description && isAnimationActive { Rectangle() .fill(.green) } else { Text(item.description) .foregroundColor(.white) .matchedGeometryEffect(id: "text\(item.description)", in: animation) } } .id("item_\(item)") // 给每个卡片添加唯一标识 .onTapGesture { withAnimation(.easeOut(duration: 0.3)) { isAnimationActive = true selectedItem = item.description } } } } .padding(.leading, 20) } .onAppear { // 返回时自动滚动到选中的卡片位置 guard let selectedNum = Int(selectedItem), !selectedItem.isEmpty else { return } proxy.scrollTo("item_\(selectedNum)", anchor: .center) } } } if isAnimationActive { ZStack { RoundedRectangle(cornerRadius: 30, style: .continuous) .fill(Color.red) .matchedGeometryEffect(id: "card\(selectedItem.description)", in: animation) .frame(width: 300, height: 400) Text(selectedItem) .foregroundColor(.white) .matchedGeometryEffect(id: "text\(selectedItem.description)", in: animation) } .transition(.offset(x: 1, y: 1)) .onTapGesture { withAnimation(.easeOut(duration: 0.3)) { isAnimationActive = false } } } } } }
方案对比
- 方法1实现更简洁,动画连贯性更好,优先推荐。
- 方法2适合需要完全移除ScrollView的场景,灵活性更高。
内容的提问来源于stack exchange,提问作者j-kobu
相关产品推荐
相关产品推荐

