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

如何创建含动态宽度元素的无限循环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)
    }
}

关键逻辑说明

  1. 动态宽度收集:通过ElementWidthPreferenceKey和GeometryReader获取每个元素渲染后的真实宽度,计算出原列表的总宽度。
  2. 内容重复:在原列表后追加一次相同元素,当用户滚动到原列表末尾时,视觉上会自然过渡到重复列表。
  3. 偏移重置:监听拖拽手势的结束状态,当偏移量超过原列表总宽度时,通过调整offset让视图瞬间跳转到对应位置,由于动画是线性的,用户感知不到跳转,看起来像是无限滚动。

是否需要UIKit?

不需要。上述方案完全基于iOS 17的SwiftUI原生组件(ScrollView、GeometryReader、PreferenceKey等)实现,无需依赖UIKit的UIScrollView或其他组件。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 21:08:22