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

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 {
    // 你的业务逻辑
}

关键修改点

  1. 保留所有LazyVGrid视图:不再用switch销毁视图,而是通过opacity控制可见性,allowsHitTesting禁用不可见视图的交互,确保SwiftUI保留每个网格的滚动状态。
  2. 为每个网格设置唯一ID:用SearchType作为id,让SwiftUI区分不同网格,避免状态混淆。
  3. 滚动偏移存储与恢复:
    • 定义ScrollOffsetKey监听滚动位置;
    • 用scrollOffsets字典存储每个标签对应的偏移量;
    • 切换标签时通过ScrollViewReader恢复之前的滚动位置。

内容的提问来源于stack exchange,提问作者Pankaj Gaikar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 00:24:31