如何在添加新项时保持ScrollView+LazyVStack的滚动位置
问题:ScrollView顶部添加内容后滚动位置偏移
我实现了如下NCMediaScrollView视图:
struct NCMediaScrollView: View, Equatable { static func == (lhs: NCMediaScrollView, rhs: NCMediaScrollView) -> Bool { return lhs.metadatas == rhs.metadatas } @Binding var metadatas: [tableMetadata] @Binding var isInSelectMode: Bool @Binding var selectedMetadatas: [tableMetadata] @Binding var columnCountStages: [Int] @Binding var columnCountStagesIndex: Int @Binding var title: String @State private var dataID: String? let queuer: Queuer let onCellSelected: (ScaledThumbnail, Bool) -> Void let onCellContextMenuItemSelected: (ScaledThumbnail, ContextMenuSelection) -> Void let onRefresh: () async -> Void var body: some View { let _ = Self._printChanges() ScrollView { LazyVStack(alignment: .leading, spacing: 2) { ForEach(metadatas.chunked(into: columnCountStages[columnCountStagesIndex]), id: \.self) { rowMetadatas in NCMediaRow(metadatas: rowMetadatas, isInSelectMode: $isInSelectMode, queuer: queuer) { tappedThumbnail, isSelected in onCellSelected(tappedThumbnail, isSelected) } onCellContextMenuItemSelected: { thumbnail, selection in onCellContextMenuItemSelected(thumbnail, selection) } .equatable() } } .padding(.top, 70) .padding(.bottom, 40) .scrollTargetLayout() } .scrollTargetBehavior(.viewAligned) .refreshable { await onRefresh() } .scrollPosition(id: $dataID, anchor: .bottom) } }
在父视图ViewModel中,通过以下代码向数组头部插入新项:
self.metadatas.insert(contentsOf: updateMetadatas.metadatasUpdate, at: 0)
插入后,ScrollView内容自动上移,无法保持原有滚动位置。已尝试iOS17的scrollTargetLayout+scrollPosition、手动重置contentOffset均无效,且因功能限制无法使用List视图。
可行解决方案
方案1:基于内容高度差手动修正偏移量
顶部新增内容会增加整体内容高度,导致原有可见区域被顶移,可通过计算新增内容的高度差,手动调整滚动位置:
- 先确定每行的固定高度(网格布局行高通常固定);
- 监听
metadatas变化,计算新增内容对应的行数与总高度; - 通过UIKit互操作调整ScrollView的
contentOffset,抵消新增高度带来的偏移。
代码示例:
// 自定义UIViewRepresentable获取ScrollView的偏移量 struct ScrollViewOffsetReader: UIViewRepresentable { @Binding var contentOffset: CGPoint func makeUIView(context: Context) -> UIScrollView { let scrollView = UIScrollView() scrollView.delegate = context.coordinator return scrollView } func updateUIView(_ uiView: UIScrollView, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, UIScrollViewDelegate { let parent: ScrollViewOffsetReader init(_ parent: ScrollViewOffsetReader) { self.parent = parent } func scrollViewDidScroll(_ scrollView: UIScrollView) { parent.contentOffset = scrollView.contentOffset } } } // 在NCMediaScrollView中集成并调整偏移 extension NCMediaScrollView { @State private var contentOffset: CGPoint = .zero @State private var pendingAdjustment: CGFloat = 0 var adjustedBody: some View { ScrollView { // 原有LazyVStack内容保持不变 } .overlay(ScrollViewOffsetReader(contentOffset: $contentOffset)) .onChange(of: metadatas) { old, new in guard new.count > old.count else { return } // 计算新增内容的高度 let addedItemCount = new.count - old.count let columnCount = columnCountStages[columnCountStagesIndex] let addedRows = Int(ceil(Double(addedItemCount) / Double(columnCount))) let rowHeight: CGFloat = 100 // 替换为实际行高 let spacing: CGFloat = 2 pendingAdjustment = CGFloat(addedRows) * (rowHeight + spacing) } .onChange(of: pendingAdjustment) { adjustment in guard adjustment > 0 else { return } DispatchQueue.main.async { contentOffset.y += adjustment pendingAdjustment = 0 } } } }
方案2:改用LazyVGrid+固定item ID修复scrollPosition失效问题
当前ForEach基于chunk后的子数组(id: \.self),新增内容会导致原有行的ID变化,使得scrollPosition无法识别目标位置。改用LazyVGrid直接遍历单个item,绑定固定ID即可解决:
// 替换原有LazyVStack为LazyVGrid var body: some View { ScrollView { LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: columnCountStages[columnCountStagesIndex]), spacing: 2) { ForEach(metadatas, id: \.id) { metadata in // 假设tableMetadata有唯一id字段 NCMediaCell(metadata: metadata, isInSelectMode: $isInSelectMode, queuer: queuer) { tappedThumbnail, isSelected in onCellSelected(tappedThumbnail, isSelected) } onCellContextMenuItemSelected: { thumbnail, selection in onCellContextMenuItemSelected(thumbnail, selection) } .equatable() } } .padding(.top, 70) .padding(.bottom, 40) .scrollTargetLayout() } .scrollTargetBehavior(.viewAligned) .refreshable { await onRefresh() } .scrollPosition(id: $dataID, anchor: .top) // 绑定到任意可见item的id }
新增内容后,原有绑定的dataID对应的item仍存在,scrollPosition会自动调整偏移量保持其可见。
内容的提问来源于stack exchange,提问作者Milen Pivchev
相关产品推荐
相关产品推荐

