iOS15/16下SwiftUI实现支持滚动吸附分页的自定义轮播
兼容iOS15/16的SwiftUI自定义轮播实现(无UIKit依赖)
可以实现,无需依赖UIKit或CollectionView,直接基于SwiftUI原生组件ScrollView+ScrollViewReader+GeometryReader组合,就能做到连续滚动+分页吸附,同时支持自定义元素间距,完美适配iOS15和16。
完整实现代码
import SwiftUI struct CarouselView<Content: View>: View { let items: [Content] let itemSpacing: CGFloat let itemWidth: CGFloat @State private var currentIndex: Int = 0 @State private var isScrolling: Bool = false init( items: [Content], itemSpacing: CGFloat = 16, itemWidth: CGFloat = UIScreen.main.bounds.width - 32 ) { self.items = items self.itemSpacing = itemSpacing self.itemWidth = itemWidth } var body: some View { ScrollView(.horizontal, showsIndicators: false) { ScrollViewReader { proxy in HStack(spacing: itemSpacing) { ForEach(0..<items.count, id: \.self) { index in items[index] .frame(width: itemWidth) .cornerRadius(12) .id(index) .overlay( GeometryReader { geo in Color.clear .onChange(of: geo.frame(in: .global).minX) { _ in guard isScrolling else { return } // 计算元素中心与屏幕中心的偏移,判断是否触发吸附 let screenCenter = UIScreen.main.bounds.width / 2 let itemCenter = geo.frame(in: .global).midX let offsetDiff = abs(itemCenter - screenCenter) if offsetDiff < itemWidth / 4 { currentIndex = index } } } ) } } .padding(.horizontal, (UIScreen.main.bounds.width - itemWidth) / 2) .onAppear { proxy.scrollTo(0, anchor: .center) } .onChange(of: currentIndex) { newIndex in withAnimation(.easeInOut(duration: 0.3)) { proxy.scrollTo(newIndex, anchor: .center) } } .gesture( DragGesture() .onChanged { _ in isScrolling = true } .onEnded { _ in isScrolling = false withAnimation(.easeInOut(duration: 0.3)) { proxy.scrollTo(currentIndex, anchor: .center) } } ) } } } } // 使用示例 struct CarouselDemoView: View { let demoItems: [some View] = (1...5).map { index in Text("轮播项 \(index)") .font(.title) .frame(height: 200) .background(Color.blue.opacity(0.3)) .cornerRadius(12) } var body: some View { VStack(spacing: 20) { Text("自定义轮播(兼容iOS15+)") .font(.headline) CarouselView( items: demoItems, itemSpacing: 12, itemWidth: UIScreen.main.bounds.width - 40 ) .frame(height: 220) } .padding() } } struct CarouselView_Previews: PreviewProvider { static var previews: some View { CarouselDemoView() } }
核心实现逻辑
- 自定义布局控制:通过
HStack的spacing参数直接设置元素间距,配合左右padding实现元素居中显示,解决TabView只能单视图填充的局限。 - 滚动状态管理:用
DragGesture标记滚动状态,避免滚动过程中频繁触发吸附逻辑,保证滚动流畅性。 - 分页吸附判断:利用
GeometryReader获取元素的全局位置,计算元素中心与屏幕中心的偏移量,当偏移小于阈值时锁定当前索引,再通过ScrollViewReader.scrollTo()实现平滑吸附。 - iOS版本兼容:
ScrollViewReader支持iOS13+,onChange支持iOS14+,完全覆盖iOS15/16需求;若需适配iOS14,可将onChange替换为onReceive监听位置变化。
可扩展优化点
- 自动轮播:添加
Timer.publish定时更新currentIndex,视图消失时记得取消定时器。 - 无限滚动:在
HStack首尾添加重复元素,滚动到边界时切换索引,实现无缝循环效果。 - 自定义吸附动画:调整
withAnimation的时长参数,适配不同场景的流畅度需求。
内容的提问来源于stack exchange,提问作者Shawn Frank
相关产品推荐
相关产品推荐

