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

如何在添加新项时保持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:基于内容高度差手动修正偏移量

顶部新增内容会增加整体内容高度,导致原有可见区域被顶移,可通过计算新增内容的高度差,手动调整滚动位置:

  1. 先确定每行的固定高度(网格布局行高通常固定);
  2. 监听metadatas变化,计算新增内容对应的行数与总高度;
  3. 通过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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 23:00:55