SwiftUI中多ScrollView切换时保存各自滚动位置的问题
解决SwiftUI切换ScrollView时滚动位置重置的问题
核心原因是切换视图时,未显示的ScrollView会被SwiftUI销毁,再次显示时重新创建,导致滚动位置丢失。下面提供两种解决方案:
方案一:iOS17+ 简洁实现(推荐)
利用iOS17新增的scrollPosition修饰符,直接绑定滚动位置,无需额外处理:
import SwiftUI // 用枚举管理当前激活的滚动视图,替代两个Bool变量,更清晰 enum ActiveScrollView { case one, two } struct TestView: View { // SwiftUI视图命名建议首字母大写 @StateObject var viewModel = Fetcher() // 类名同样建议首字母大写 @State private var activeView: ActiveScrollView = .one // 存储两个滚动视图的滚动位置(绑定到列表元素的id) @State private var scrollPosition1: Int? @State private var scrollPosition2: Int? var body: some View { VStack { HStack { Spacer() Button { activeView = .one } label: { ZStack{ Color.blue Text("scroll view 1") .foregroundColor(.white) } .frame(width: 100, height: 100) } Button { activeView = .two } label: { ZStack{ Color.blue Text("scroll view 2") .foregroundColor(.white) } .frame(width: 100, height: 100) } Spacer() } .padding(.bottom) if activeView == .one { ScrollView { LazyVStack { ForEach(viewModel.content.indices, id: \.self) { i in SomeView(content: i) // 组件名首字母大写 .id(i) // 必须给每个元素添加唯一id,让scrollPosition识别位置 } } .scrollPosition(id: $scrollPosition1) // 绑定滚动位置 } } else { ScrollView { LazyVStack { ForEach(viewModel.content.indices, id: \.self) { i in SomeView(content: i) .id(i) } } .scrollPosition(id: $scrollPosition2) } } } } } // 补充示例所需的类和组件(原代码中缺失) class Fetcher: ObservableObject { @Published var content = Array(0...100) // 模拟长列表数据 } struct SomeView: View { let content: Int var body: some View { Text("Item \(content)") .frame(height: 50) .background(.gray.opacity(0.2)) .padding(.horizontal) } }
方案二:兼容iOS16及以下版本
通过ScrollViewReader和自定义PreferenceKey捕获滚动偏移,手动保存和恢复位置:
import SwiftUI enum ActiveScrollView { case one, two } // 自定义PreferenceKey,用于捕获ScrollView的滚动偏移 struct ScrollOffsetKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {} } struct TestView: View { @StateObject var viewModel = Fetcher() @State private var activeView: ActiveScrollView = .one // 存储两个滚动视图的滚动偏移 @State private var scrollOffset1: CGFloat = 0 @State private var scrollOffset2: CGFloat = 0 var body: some View { VStack { HStack { Spacer() Button { activeView = .one } label: { ZStack{ Color.blue Text("scroll view 1") .foregroundColor(.white) } .frame(width: 100, height: 100) } Button { activeView = .two } label: { ZStack{ Color.blue Text("scroll view 2") .foregroundColor(.white) } .frame(width: 100, height: 100) } Spacer() } .padding(.bottom) if activeView == .one { ScrollViewReader { proxy in ScrollView { LazyVStack { ForEach(viewModel.content.indices, id: \.self) { i in SomeView(content: i) } } .background( GeometryReader { geo in Color.clear .preference(key: ScrollOffsetKey.self, value: geo.frame(in: .named("scrollView")).minY) } ) .onPreferenceChange(ScrollOffsetKey.self) { offset in // 实时保存滚动偏移 scrollOffset1 = -offset } } .coordinateSpace(name: "scrollView") .onAppear { // 恢复滚动位置 withAnimation { proxy.scrollTo(scrollOffset1, anchor: .top) } } } } else { ScrollViewReader { proxy in ScrollView { LazyVStack { ForEach(viewModel.content.indices, id: \.self) { i in SomeView(content: i) } } .background( GeometryReader { geo in Color.clear .preference(key: ScrollOffsetKey.self, value: geo.frame(in: .named("scrollView2")).minY) } ) .onPreferenceChange(ScrollOffsetKey.self) { offset in scrollOffset2 = -offset } } .coordinateSpace(name: "scrollView2") .onAppear { withAnimation { proxy.scrollTo(scrollOffset2, anchor: .top) } } } } } } } // 补充示例类和组件 class Fetcher: ObservableObject { @Published var content = Array(0...100) } struct SomeView: View { let content: Int var body: some View { Text("Item \(content)") .frame(height: 50) .background(.gray.opacity(0.2)) .padding(.horizontal) } }
关键说明
- 用枚举替代两个Bool变量:避免状态冲突,代码更易维护。
- 保存滚动位置:每个ScrollView对应独立的位置变量,切换时不会互相干扰。
- 视图命名规范:Swift中类型名建议首字母大写,这是官方推荐的编码规范。
内容的提问来源于stack exchange,提问作者Ahmed Zaidan
相关产品推荐
相关产品推荐

