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

SwiftUI水平ScrollView如何实现无跳转的无限循环滚动?

问题分析与修正方案

你的代码存在几个关键问题导致无限循环滚动无法正常工作,以下是问题点和修正后的实现:

核心问题点

  • 变量名不一致与未定义变量:定义的timePosition被错误写成timeScrollPosition,且代码中出现未定义的model变量,直接导致编译失败。
  • 滚动逻辑错误:两种滚动方向都使用了相同的rotate(toStartAt: times.count-1)操作,向右滚动到开头时的逻辑完全错误,无法实现反向循环。
  • 滚动位置跳转问题:修改数组后直接调用scrollTo(id: currentViewID)会导致视觉跳转,因为数组结构变化后原ID的位置已改变。
  • ID冲突风险:如果直接追加重复的时间值(比如把0再次加到数组末尾),会导致ForEach的ID重复,SwiftUI无法正确追踪视图。

修正后的实现

以下是调整后的代码,实现平滑的无限循环滚动:

import SwiftUI
import Algorithms

// 用结构体包装时间值,确保每个元素有唯一ID
struct TimeItem: Identifiable {
    let id = UUID()
    let value: Int
}

struct ContentView: View {
    @State private var scrollPosition: ScrollPosition<UUID> = .init(idType: UUID.self, edge: .center)
    @State private var times: [TimeItem] = Array(0...23).map { TimeItem(value: $0) }
    // 标记是否正在更新数组,避免重复触发
    @State private var isUpdating = false
    
    // 每次更新数组的元素数量,根据可视区域调整
    private let updateCount = 3
    
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            LazyHStack(spacing: 0) {
                ForEach(times) { time in
                    Text("\(time.value)")
                        .font(.system(.callout, design: .monospaced, weight: .semibold))
                        .padding(8)
                        .frame(width: 180, height: 110, alignment: .topLeading)
                        .border(width: 1, edges: [.leading, .trailing], color: .primary.opacity(0.05))
                }
            }
            .scrollTargetLayout()
        }
        .frame(height: 110)
        .scrollPosition($scrollPosition, anchor: .center)
        .onScrollTargetVisibilityChange(idType: UUID.self) { visibleIDs in
            guard !isUpdating, let currentID = scrollPosition.viewID else { return }
            
            // 向左滚动接近末尾:将开头的元素追加到末尾
            if visibleIDs.contains(times[times.count - updateCount].id) {
                isUpdating = true
                let newItems = times.prefix(updateCount).map { TimeItem(value: $0.value) }
                times.append(contentsOf: newItems)
                // 保持当前元素在中心位置
                scrollPosition.scrollTo(id: currentID, anchor: .center)
                isUpdating = false
            }
            
            // 向右滚动接近开头:将末尾的元素添加到开头
            if visibleIDs.contains(times[updateCount - 1].id) {
                isUpdating = true
                let newItems = times.suffix(updateCount).map { TimeItem(value: $0.value) }
                times.insert(contentsOf: newItems, at: 0)
                // 保持当前元素在中心位置
                scrollPosition.scrollTo(id: currentID, anchor: .center)
                isUpdating = false
            }
        }
    }
}

// 扩展View,实现border指定边的功能(原代码中用到)
extension View {
    func border(width: CGFloat, edges: [Edge], color: Color) -> some View {
        overlay(EdgeBorder(width: width, edges: edges).foregroundColor(color))
    }
}

struct EdgeBorder: Shape {
    var width: CGFloat
    var edges: [Edge]
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        for edge in edges {
            switch edge {
            case .top:
                path.move(to: CGPoint(x: rect.minX, y: rect.minY))
                path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
            case .bottom:
                path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
                path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
            case .leading:
                path.move(to: CGPoint(x: rect.minX, y: rect.minY))
                path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
            case .trailing:
                path.move(to: CGPoint(x: rect.maxX, y: rect.minY))
                path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
            }
        }
        return path.strokedPath(.init(lineWidth: width))
    }
}

关键修改说明

  1. 使用Identifiable结构体:避免ID重复问题,每个滚动项有唯一的UUID,SwiftUI能正确追踪视图变化。
  2. 修正变量名与移除未定义变量:统一使用scrollPosition,去掉不存在的model引用。
  3. 双向循环逻辑:
    • 向左滚动接近末尾时,将数组开头的updateCount个元素复制追加到末尾
    • 向右滚动接近开头时,将数组末尾的updateCount个元素复制添加到开头
  4. 防止重复触发:添加isUpdating标记,避免数组更新过程中重复触发滚动事件。
  5. 保持滚动位置平滑:每次更新数组后,立即将滚动位置定位回当前显示的中心元素,视觉上无跳转。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 23:30:13