SwiftUI单视图多垂直ScrollView滚动偏移同步问题求助
标签式LazyVGrid保留滚动位置解决方案
问题描述
要实现标签式布局,用单个ScrollView嵌套多个LazyVGrid承载三种不同数据类型。当前切换标签时,ScrollView会重置滚动偏移,且不同标签的滚动偏移同步。尝试过两种方案均无效:
- 将LazyVGrid定义为变量,但因数据归属ViewModel且实际场景中LazyVGrid为独立视图,无法实现;
- 每个标签对应一个ScrollView,但切换标签时SwiftUI会强制LazyVGrid重新加载,回到偏移0的位置。
原代码
import SwiftUI enum SearchType: String, CaseIterable { case movie = "Movies" case tv = "TV Shows" case people = "People" } struct SearchContainerView: View { @ObservedObject var viewModel = SearchViewModel() @State var currentSelectedSearchType: SearchType = .movie let array: [String] = ["The", "above", "works", "great", "when", "you", "know", "where", "in", "the", "array", "the", "value" ,"is", "that", "is", "when" ,"you", "know", "its", "index", "value", "As", "the", "index", "values", "begin", "at" ,"0", "the" ,"second", "entry", "will", "be", "at", "index", "1"] var body: some View { NavigationView { VStack { HStack { ForEach(SearchType.allCases, id: \.self) { type in HStack { Spacer() Text(type.rawValue) .font(.title3) .onTapGesture { self.currentSelectedSearchType = type } Spacer() } .padding(5) .background(currentSelectedSearchType == type ? Color.gray : Color.clear) .cornerRadius(10) } .background(Color.gray.opacity(0.5)) .cornerRadius(10) .padding(.horizontal) } ScrollView { switch currentSelectedSearchType { case .movie: LazyVGrid(columns: [ GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()) ], content: { ForEach(array, id: \.self) { Text($0).font(.largeTitle).bold().frame(width: UIScreen.main.bounds.width / 3, height: 100, alignment: .center) } }) case .tv: LazyVGrid(columns: [ GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()) ], content: { ForEach(array, id: \.self) { Text($0).font(.largeTitle).bold().frame(width: UIScreen.main.bounds.width / 3, height: 100, alignment: .center) } }) case .people: LazyVGrid(columns: [ GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()) ], content: { ForEach(array, id: \.self) { Text($0).font(.largeTitle).bold().frame(width: UIScreen.main.bounds.width / 3, height: 100, alignment: .center) } }) } } } } }
解决方案
核心思路是保留所有LazyVGrid的视图状态,通过透明度控制显示/隐藏,同时为每个标签单独保存滚动偏移量,切换时恢复对应位置。
修改后完整代码
import SwiftUI enum SearchType: String, CaseIterable { case movie = "Movies" case tv = "TV Shows" case people = "People" } // 用于监听滚动偏移的PreferenceKey struct ScrollOffsetKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } } struct SearchContainerView: View { @ObservedObject var viewModel = SearchViewModel() @State var currentSelectedSearchType: SearchType = .movie // 存储每个标签的滚动偏移量 @State private var scrollOffsets: [SearchType: CGFloat] = [:] let array: [String] = ["The", "above", "works", "great", "when", "you", "know", "where", "in", "the", "array", "the", "value" ,"is", "that", "is", "when" ,"you", "know", "its", "index", "value", "As", "the", "index", "values", "begin", "at" ,"0", "the" ,"second", "entry", "will", "be", "at", "index", "1"] // 提取网格列配置,避免重复代码 private let gridColumns: [GridItem] = [ GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()) ] var body: some View { NavigationView { VStack { // 标签栏 HStack { ForEach(SearchType.allCases, id: \.self) { type in HStack { Spacer() Text(type.rawValue) .font(.title3) .onTapGesture { currentSelectedSearchType = type } Spacer() } .padding(5) .background(currentSelectedSearchType == type ? Color.gray : Color.clear) .cornerRadius(10) } .background(Color.gray.opacity(0.5)) .cornerRadius(10) .padding(.horizontal) } // 带滚动偏移管理的ScrollView ScrollViewReader { proxy in ScrollView { // Movies网格 LazyVGrid(columns: gridColumns, content: { ForEach(array, id: \.self) { item in Text(item) .font(.largeTitle) .bold() .frame(width: UIScreen.main.bounds.width / 3, height: 100, alignment: .center) } }) .id(SearchType.movie) .opacity(currentSelectedSearchType == .movie ? 1 : 0) .allowsHitTesting(currentSelectedSearchType == .movie) // TV Shows网格 LazyVGrid(columns: gridColumns, content: { ForEach(array, id: \.self) { item in Text(item) .font(.largeTitle) .bold() .frame(width: UIScreen.main.bounds.width / 3, height: 100, alignment: .center) } }) .id(SearchType.tv) .opacity(currentSelectedSearchType == .tv ? 1 : 0) .allowsHitTesting(currentSelectedSearchType == .tv) // People网格 LazyVGrid(columns: gridColumns, content: { ForEach(array, id: \.self) { item in Text(item) .font(.largeTitle) .bold() .frame(width: UIScreen.main.bounds.width / 3, height: 100, alignment: .center) } }) .id(SearchType.people) .opacity(currentSelectedSearchType == .people ? 1 : 0) .allowsHitTesting(currentSelectedSearchType == .people) } // 切换标签时恢复滚动位置 .onChange(of: currentSelectedSearchType) { newType in if let offset = scrollOffsets[newType] { proxy.scrollTo(offset, anchor: .top) } } // 初始化偏移量 .onAppear { for type in SearchType.allCases { scrollOffsets[type] = scrollOffsets[type] ?? 0 } } // 监听滚动偏移并保存 .background( GeometryReader { geo in Color.clear .preference(key: ScrollOffsetKey.self, value: geo.frame(in: .global).minY) } ) .onPreferenceChange(ScrollOffsetKey.self) { offset in scrollOffsets[currentSelectedSearchType] = -offset } } } } } } // 原ViewModel(保持不变) class SearchViewModel: ObservableObject { // 你的业务逻辑 }
关键修改点
- 保留所有LazyVGrid视图:不再用
switch销毁视图,而是通过opacity控制可见性,allowsHitTesting禁用不可见视图的交互,确保SwiftUI保留每个网格的滚动状态。 - 为每个网格设置唯一ID:用
SearchType作为id,让SwiftUI区分不同网格,避免状态混淆。 - 滚动偏移存储与恢复:
- 定义
ScrollOffsetKey监听滚动位置; - 用
scrollOffsets字典存储每个标签对应的偏移量; - 切换标签时通过
ScrollViewReader恢复之前的滚动位置。
- 定义
内容的提问来源于stack exchange,提问作者Pankaj Gaikar
相关产品推荐
相关产品推荐

