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

SwiftUI ScrollView实现滚动至上一个/下一个条目的功能问询

实现滚动到上一个/下一个条目的解决方案

可以实现这个功能,核心是通过跟踪当前第一个可见条目,结合ScrollViewReader(或iOS17+的scrollPosition)完成精准滚动。以下是完整实现代码及关键说明:

import SwiftUI
import PlaygroundSupport

// 用于收集条目位置信息的PreferenceKey
struct VisibleItemPreferenceKey: PreferenceKey {
    static var defaultValue: [Int: CGRect] = [:]
    
    static func reduce(value: inout [Int: CGRect], nextValue: () -> [Int: CGRect]) {
        value.merge(nextValue()) { $1 }
    }
}

struct ContentView: View {
    @State var items = 1...100
    @State private var currentFirstVisibleItem: Int = 1
    @State private var scrollViewFrame: CGRect = .zero
    
    var body: some View {
        VStack {
            HStack {
                Button("Scroll to previous") {
                    guard items.contains(currentFirstVisibleItem - 1) else { return }
                    currentFirstVisibleItem -= 1
                }

                Button("Scroll to next") {
                    guard items.contains(currentFirstVisibleItem + 1) else { return }
                    currentFirstVisibleItem += 1
                }
            }
            .padding()

            // iOS17+ 用scrollPosition简化滚动
            ScrollView(.vertical) {
                LazyVStack(alignment: .leading, spacing: 16) {
                    ForEach(items, id: \.self) { item in
                        Text(String(item))
                            .frame(width: 50, height: 50)
                            .background(Color.gray.opacity(0.2))
                            .background(
                                GeometryReader { geo in
                                    Color.clear
                                        .preference(
                                            key: VisibleItemPreferenceKey.self,
                                            value: [item: geo.frame(in: .named("scrollView"))]
                                        )
                                }
                            )
                    }
                }
            }
            .frame(height: 300)
            .coordinateSpace(name: "scrollView")
            .background(
                GeometryReader { geo in
                    Color.clear
                        .onAppear { scrollViewFrame = geo.frame(in: .global) }
                        .onChange(of: geo.frame(in: .global)) { scrollViewFrame = $0 }
                }
            )
            .onPreferenceChange(VisibleItemPreferenceKey.self) { preferences in
                // 筛选可见条目并找到最顶部的一个
                let visibleItems = preferences.filter { _, frame in
                    scrollViewFrame.intersects(frame)
                }
                .sorted { $0.value.minY < $1.value.minY }
                
                if let firstVisible = visibleItems.first?.key {
                    currentFirstVisibleItem = firstVisible
                }
            }
            .scrollPosition(id: $currentFirstVisibleItem)
            
            // iOS16及以下替换为ScrollViewReader写法:
            /*
            ScrollViewReader { proxy in
                ScrollView(.vertical) {
                    LazyVStack(alignment: .leading, spacing: 16) {
                        // 原有条目内容不变
                    }
                }
                // 其他监听逻辑不变
                .onChange(of: currentFirstVisibleItem) { item in
                    proxy.scrollTo(item, anchor: .top)
                }
            }
            */
        }
    }
}

PlaygroundPage.current.setLiveView(ContentView())

关键实现逻辑

  1. 跟踪条目位置:通过自定义VisibleItemPreferenceKey,用GeometryReader收集每个条目在ScrollView坐标系内的位置,实时更新位置数据。
  2. 识别第一个可见条目:监听位置数据变化,筛选出和ScrollView可见区域相交的条目,按Y轴坐标排序后取最顶部的条目ID,更新currentFirstVisibleItem状态。
  3. 控制滚动:按钮点击时,基于当前第一个可见条目ID计算上一个/下一个条目ID,通过scrollPosition(或ScrollViewReader.scrollTo)触发滚动,确保目标条目滚动到顶部位置。

这个方案支持手动滚动后自动更新基准条目,完全符合需求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 14:35:28