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

SwiftUI:如何判断ForEach循环内列表的首项是否可见?

SwiftUI实现滚动到顶部按钮(仅非顶部时显示)

iOS 17+ 简洁实现

利用iOS17新增的scrollPosition API,可以直接绑定列表的滚动位置,轻松判断是否处于顶部:

struct ScrollToTopDemo: View {
    // 模拟列表数据
    private let listItems = Array(1...100).map { "条目 \($0)" }
    @State private var currentScrollPosition: Int?
    @State private var showTopButton = false
    
    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            List {
                ForEach(listItems.indices, id: \.self) { index in
                    Text(listItems[index])
                        .id(index) // 给每个条目设置唯一ID,用于滚动定位
                }
            }
            // 绑定滚动位置到currentScrollPosition
            .scrollPosition(id: $currentScrollPosition)
            .onChange(of: currentScrollPosition) { _, newPosition in
                // 当滚动位置不是第一个条目时,显示按钮
                showTopButton = newPosition != 0
            }
            
            // 滚动到顶部按钮
            if showTopButton {
                Button(action: {
                    withAnimation(.easeInOut) {
                        currentScrollPosition = 0
                    }
                }) {
                    Image(systemName: "arrow.up.circle.fill")
                        .font(.title)
                        .foregroundColor(.blue)
                        .padding()
                        .background(Color.white.opacity(0.8))
                        .clipShape(Circle())
                        .shadow(radius: 4)
                }
                .padding(.trailing, 16)
                .padding(.bottom, 16)
            }
        }
    }
}

iOS 16及以下兼容方案

对于旧系统,我们可以通过PreferenceKey结合GeometryReader来检测第一个条目的可见性:

首先定义一个用于传递可见性状态的PreferenceKey:

struct FirstItemVisibleKey: PreferenceKey {
    static var defaultValue: Bool = true
    
    static func reduce(value: inout Bool, nextValue: () -> Bool) {
        value = value && nextValue()
    }
}

然后实现主视图:

struct ScrollToTopDemo: View {
    private let listItems = Array(1...100).map { "条目 \($0)" }
    @State private var showTopButton = false
    @State private var scrollProxy: ScrollViewProxy?
    
    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            List {
                ForEach(listItems.indices, id: \.self) { index in
                    Text(listItems[index])
                        .id(index)
                        .background(
                            GeometryReader { geo in
                                Color.clear
                                    .preference(
                                        key: FirstItemVisibleKey.self,
                                        value: self.isFirstItemVisible(geo: geo)
                                    )
                            }
                        )
                }
            }
            .onPreferenceChange(FirstItemVisibleKey.self) { isVisible in
                showTopButton = !isVisible
            }
            .scrollViewReader { proxy in
                scrollProxy = proxy
            }
            
            if showTopButton {
                Button(action: {
                    withAnimation(.easeInOut) {
                        scrollProxy?.scrollTo(0, anchor: .top)
                    }
                }) {
                    Image(systemName: "arrow.up.circle.fill")
                        .font(.title)
                        .foregroundColor(.blue)
                        .padding()
                        .background(Color.white.opacity(0.8))
                        .clipShape(Circle())
                        .shadow(radius: 4)
                }
                .padding(.trailing, 16)
                .padding(.bottom, 16)
            }
        }
    }
    
    // 判断第一个条目是否处于屏幕顶部可见区域
    private func isFirstItemVisible(geo: GeometryProxy) -> Bool {
        let safeAreaTop = UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0
        return geo.frame(in: .global).minY <= UIScreen.main.bounds.minY + safeAreaTop
    }
}

关键逻辑说明

  • 显示/隐藏控制:通过判断滚动位置是否为第一个条目(iOS17+),或第一个条目是否在屏幕顶部可见区域(旧系统),来切换按钮的显示状态。
  • 滚动到顶部:借助ScrollViewReader的scrollTo方法(或iOS17+直接设置scrollPosition),配合动画实现平滑滚动。
  • 按钮样式:采用悬浮圆形按钮,保证在列表之上且不遮挡内容。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 05:05:52