SwiftUI List数据加载自动滚顶及滑动操作异常问题
SwiftUI List 问题解决方案
1. 阻止数据更新时自动滚顶,保持滚动位置
原因分析
数据更新(尤其是重置historyItems)时,List会重新渲染整个结构,且未保留滚动位置逻辑,导致自动回到顶部。另外,当前用VStack嵌套分组内容,未利用SwiftUI List原生Section结构,也会加剧滚动位置不稳定。
解决方案
使用ScrollViewReader记录并恢复滚动位置,同时改用List原生Section优化结构,提升稳定性。
步骤1:在父视图中整合ScrollViewReader
struct YourMainView: View { @ObservedObject var viewModel: HistoryViewModel @State private var lastScrollItemId: String? // 保存最后可见的item唯一标识 var body: some View { NavigationView { ScrollViewReader { proxy in List { historySection .onAppear { // 恢复上一次的滚动位置 if let itemId = lastScrollItemId { proxy.scrollTo(itemId, anchor: .top) } } } .onChange(of: viewModel.historyItems) { _ in // 数据更新前记录当前可见的第一个item ID let visibleIds = proxy.visibleIds lastScrollItemId = visibleIds.first?.stringValue } } } } // 改用List原生Section替代VStack分组 private var historySection: some View { Group { if viewModel.isLoading || viewModel.isLoadingMore { ForEach(0..<3, id: \.self) { _ in SkeletonHistorySectionView() } } else { ForEach(groupedItems, id: \.key) { date, items in Section(header: Text(viewModel.dateFormatter.string(from: date)) .font(.footnote) .foregroundColor(.gray) .padding(.leading) .frame(maxWidth: .infinity) .padding(.top) ) { ForEach(items) { item in HistoryRow(item: item, dateFormatter: viewModel.dateFormatter, timeFormatter: viewModel.timeFormatter, categoryDictionary: categoryDictionary, accountDictionary: accountDictionary, viewModel: viewModel) .id(item.id) // 绑定item唯一ID,确保行身份稳定 .onTapGesture { selectedItem = item showingDetailSheet.toggle() } .swipeActions { Button { handleSwipeAction(for: item) } label: { Label("TEST.", systemImage: "pencil") } .tint(.accentColor) } } } .padding(.bottom, 8) } } } } // 单独处理当前item的滑动操作 private func handleSwipeAction(for item: HistoryItem) { // 这里写针对当前item的逻辑,比如编辑、删除 viewModel.processItemAction(item) } }
步骤2:优化数据加载逻辑
避免清空数组后重新追加,直接替换重置场景下的数据:
func loadMoreHistoryData(reset: Bool = false, reportMonth: String, reportCategoryId: Int = 0) async { if isLoadingMore { return } isLoadingMore = true defer { isLoadingMore = false } do { let (newItems, totalIncome, totalExpenses) = try await dataService.getRecentTransactions(page: currentPage, size: pageSize, reportMonth: reportMonth, reportCategoryId: reportCategoryId) DispatchQueue.main.async { self.totalIncome = totalIncome self.totalExpenses = totalExpenses if reset { self.historyItems = newItems // 直接替换数组而非清空再追加 self.currentPage = 2 } else { self.historyItems.append(contentsOf: newItems) self.currentPage += 1 } if newItems.count < self.pageSize { self.isAllDataLoaded = true } } } catch { DispatchQueue.main.async { self.errorMessage = error.localizedDescription } } }
2. 限制滑动操作仅作用于当前交互项
原因分析
HistoryRow持有全局@ObservedObject viewModel,若滑动操作依赖viewModel的共享状态,会导致所有行响应变化;另外,若HistoryItem未遵循Identifiable,ForRow的ID不稳定会引发SwiftUI误判行身份,导致批量滑动问题。
解决方案
绑定滑动操作到当前item,确保HistoryItem身份唯一稳定,减少全局状态依赖。
步骤1:让HistoryItem遵循Identifiable
struct HistoryItem: Identifiable { let id: String // 或Int,确保是唯一且不变的标识 // 其他属性:title, amount, type, iconName等 }
步骤2:移除HistoryRow的全局视图模型监听(可选)
若HistoryRow不需要监听viewModel全局变化,改为常量引用减少不必要重绘:
struct HistoryRow: View { let item: HistoryItem let dateFormatter: DateFormatter let timeFormatter: DateFormatter let categoryDictionary: [Int: Category] let accountDictionary: [Int: Account] let viewModel: HistoryViewModel // 改为let,而非@ObservedObject // ... 其余代码不变 }
内容的提问来源于stack exchange,提问作者Haji Lazgiyev
相关产品推荐
相关产品推荐

