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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 01:44:54