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

SwiftUI横向ScrollView增删Item时如何保持X轴滚动位置?

SwiftUI横向ScrollView增删Item后保持滚动位置的解决方案

问题核心

增删Item时,ScrollView因内容结构变更会自动重置滚动偏移,需手动保存并恢复偏移量,同时根据内容宽度变化调整偏移值,维持视觉上的滚动位置不变。

解决步骤

1. 保存当前滚动偏移

在ItemView中添加状态变量存储实时滚动偏移,修改滚动回调方法保存偏移:

public struct ItemView: View {
    var action: () -> Void
    @ObservedObject var itemData: ItemData
    @State private var isReducedSize: Bool = false
    @State private var currentOffset = CGPoint.zero // 新增:保存当前滚动偏移

    public init(action: @escaping () -> Void, itemData: ItemData) {
        self.action = action
        self.itemData = itemData
    }

    private func handleScroll(_ offset: CGPoint) {
        currentOffset = offset // 实时保存偏移
        withAnimation {
            isReducedSize = offset.x >= -100
        }
    }
}

2. 修改ScrollViewWithOffset支持偏移控制

针对自定义的ScrollViewWithOffset,添加绑定变量控制滚动偏移,确保内容更新后自动恢复偏移:

struct ScrollViewWithOffset: UIViewRepresentable {
    var content: () -> UIView
    var onScroll: (CGPoint) -> Void
    @Binding var scrollOffset: CGPoint // 新增:绑定外部偏移量

    func makeUIView(context: Context) -> UIScrollView {
        let scrollView = UIScrollView()
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.showsVerticalScrollIndicator = false
        let contentView = content()
        scrollView.addSubview(contentView)
        contentView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
            contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            contentView.heightAnchor.constraint(equalTo: scrollView.heightAnchor)
        ])
        scrollView.delegate = context.coordinator
        return scrollView
    }

    func updateUIView(_ uiView: UIScrollView, context: Context) {
        // 内容更新后恢复之前的滚动偏移
        if uiView.contentOffset != scrollOffset {
            uiView.contentOffset = scrollOffset
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, UIScrollViewDelegate {
        var parent: ScrollViewWithOffset
        init(_ parent: ScrollViewWithOffset) {
            self.parent = parent
        }
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            parent.onScroll(scrollView.contentOffset)
        }
    }
}

3. 绑定偏移并调整增删后的偏移量

在ItemView中关联偏移绑定,同时监听Item增删事件,根据内容宽度变化调整偏移:

public var body: some View {
    HStack {
        ScrollViewWithOffset(
            content: {
                UIHostingController(rootView: ItemCollectionView(itemData: itemData)).view
            },
            onScroll: handleScroll,
            scrollOffset: $currentOffset
        )
    }
    .padding(8)
    .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("AdjustScrollOffset"))) { notification in
        if let widthDelta = notification.object as? CGFloat {
            currentOffset.x += widthDelta
        }
    }
}

4. 在ItemData中触发偏移调整通知

修改ItemData的增删方法,根据Item宽度发送偏移调整通知:

class ItemData: ObservableObject {
    @Published var items: [Item] = []
    let itemWidth: CGFloat = 80 // 根据实际Item宽度修改

    func add(_ item: Item) {
        items.append(item)
        // 添加Item后内容宽度增加,偏移需同步调整
        NotificationCenter.default.post(name: NSNotification.Name("AdjustScrollOffset"), object: itemWidth)
    }

    func remove(_ item: Item) {
        guard let index = items.firstIndex(where: { $0.id == item.id }) else { return }
        items.remove(at: index)
        // 删除Item后内容宽度减少,偏移需同步调整
        NotificationCenter.default.post(name: NSNotification.Name("AdjustScrollOffset"), object: -itemWidth)
    }
}

iOS 17+简化方案

若项目适配iOS 17及以上,可直接使用官方scrollPosition修饰符,无需自定义ScrollView:

ScrollView(.horizontal) {
    HStack {
        ForEach(itemData.items, id: \.id) { item in
            itemView(
                content: itemViewContentProvider.content(
                    item: item,
                    onitemTapped: { item in itemData.add(item) },
                    onDelete: { item in itemData.remove(item) }
                )
            )
        }
    }
    .scrollTargetLayout()
}
.scrollPosition(id: $lastSelectedItemId) // 绑定最后选中的Item ID,增删后保持指向同一Item

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 00:23:21