如何创建含动态宽度元素的无限循环Horizontal ScrollView
动态宽度元素的无限横向滚动实现方案(iOS 17)
旧方案的调整可能性
如果之前的方案是基于固定宽度元素实现的无限滚动,可以调整适配动态宽度元素,但核心需要补充「动态获取元素实际渲染宽度」的逻辑。旧方案通常依赖固定宽度计算滚动偏移和内容重复位置,只要通过SwiftUI的偏好设置(Preference)或GeometryReader拿到每个元素的真实宽度,就能重新计算总滚动范围,进而调整偏移重置的逻辑。
iOS 17原生API实现(无需UIKit)
完全可以仅用iOS 17的SwiftUI API实现,不需要引入UIKit。核心思路是:
- 收集每个动态宽度元素的实际渲染尺寸
- 在ScrollView中渲染原元素列表 + 一次重复列表,实现视觉衔接
- 监听滚动偏移,当滚动至重复列表区域时,通过偏移量重置让视图无缝跳转到对应原列表位置,模拟无限滚动效果
示例代码
// 用于收集元素宽度的偏好键 struct ElementWidthPreferenceKey: PreferenceKey { static var defaultValue: [CGFloat] = [] static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) { value.append(contentsOf: nextValue()) } } struct InfiniteDynamicWidthScrollView: View { let items: [String] @State private var originalElementWidths: [CGFloat] = [] @State private var totalOriginalWidth: CGFloat = 0 @State private var currentOffset: CGFloat = 0 private let itemSpacing: CGFloat = 16 var body: some View { ScrollView(.horizontal) { HStack(spacing: itemSpacing) { // 渲染原列表并收集宽度 ForEach(Array(items.enumerated()), id: \.offset) { index, text in Text(text) .padding(.horizontal, 16) .padding(.vertical, 8) .background(Color.gray.opacity(0.2)) .cornerRadius(8) .background( GeometryReader { geo in Color.clear .preference(key: ElementWidthPreferenceKey.self, value: [geo.size.width]) } ) } // 渲染重复列表,实现衔接 ForEach(items, id: \.self) { text in Text(text) .padding(.horizontal, 16) .padding(.vertical, 8) .background(Color.gray.opacity(0.2)) .cornerRadius(8) } } .padding(.horizontal, 16) .onPreferenceChange(ElementWidthPreferenceKey.self) { allWidths in // 仅保留原列表的元素宽度 originalElementWidths = Array(allWidths.prefix(items.count)) // 计算原列表总宽度(含间距) totalOriginalWidth = originalElementWidths.reduce(0, +) + CGFloat(items.count - 1) * itemSpacing } .offset(x: currentOffset) .gesture( DragGesture() .onChanged { gesture in currentOffset = gesture.translation.width } .onEnded { gesture in let finalOffset = currentOffset + gesture.predictedEndTranslation.width // 滚动到重复列表末尾时,重置偏移到原列表起始位置 if finalOffset < -totalOriginalWidth { withAnimation(.linear) { currentOffset += totalOriginalWidth } } // 反向滚动到原列表开头时,重置偏移到重复列表起始位置 else if finalOffset > 0 { withAnimation(.linear) { currentOffset -= totalOriginalWidth } } } ) } .scrollIndicators(.hidden) } }
关键逻辑说明
- 动态宽度收集:通过
ElementWidthPreferenceKey和GeometryReader获取每个元素渲染后的真实宽度,计算出原列表的总宽度。 - 内容重复:在原列表后追加一次相同元素,当用户滚动到原列表末尾时,视觉上会自然过渡到重复列表。
- 偏移重置:监听拖拽手势的结束状态,当偏移量超过原列表总宽度时,通过调整
offset让视图瞬间跳转到对应位置,由于动画是线性的,用户感知不到跳转,看起来像是无限滚动。
是否需要UIKit?
不需要。上述方案完全基于iOS 17的SwiftUI原生组件(ScrollView、GeometryReader、PreferenceKey等)实现,无需依赖UIKit的UIScrollView或其他组件。
内容的提问来源于stack exchange,提问作者Corith
相关产品推荐
相关产品推荐

