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)) } }
关键修改说明
- 使用
Identifiable结构体:避免ID重复问题,每个滚动项有唯一的UUID,SwiftUI能正确追踪视图变化。 - 修正变量名与移除未定义变量:统一使用
scrollPosition,去掉不存在的model引用。 - 双向循环逻辑:
- 向左滚动接近末尾时,将数组开头的
updateCount个元素复制追加到末尾 - 向右滚动接近开头时,将数组末尾的
updateCount个元素复制添加到开头
- 向左滚动接近末尾时,将数组开头的
- 防止重复触发:添加
isUpdating标记,避免数组更新过程中重复触发滚动事件。 - 保持滚动位置平滑:每次更新数组后,立即将滚动位置定位回当前显示的中心元素,视觉上无跳转。
内容的提问来源于stack exchange,提问作者HeySaiK
相关产品推荐
相关产品推荐

