如何在SwiftUI中实现自定义横向滚动星座选择器?
iOS 横向星座卡片选择器实现方案
核心思路
放弃UIPickerView,改用UICollectionView实现横向滚动卡片效果,更灵活适配需求:
- 横向流式布局,控制item间距与尺寸,确保卡片滚动时能精准对齐中心
- 添加固定在CollectionView中心的金色圆圈指示器
- 监听滚动状态,自动将最近的卡片居中对齐
- 根据卡片与中心的位置动态调整透明度
具体实现步骤
1. 基础布局配置
1.1 创建UICollectionView与横向布局
class ZodiacSelectorViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { private var collectionView: UICollectionView! private let zodiacImages = ["aries", "taurus", "gemini", "cancer", "leo", "virgo", "libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces"] private var selectedZodiacIndex = 0 // 默认选中第一个星座 override func viewDidLoad() { super.viewDidLoad() setupCollectionView() setupCenterIndicator() } private func setupCollectionView() { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.itemSize = CGSize(width: 120, height: 120) // 按需调整卡片大小 layout.minimumLineSpacing = 40 // 卡片间距 layout.sectionInset = UIEdgeInsets(top: 0, left: (view.bounds.width - 120)/2, bottom: 0, right: (view.bounds.width - 120)/2) // 左右边距,让第一个卡片初始居中 collectionView = UICollectionView(frame: CGRect(x: 0, y: view.bounds.height/2 - 60, width: view.bounds.width, height: 120), collectionViewLayout: layout) collectionView.dataSource = self collectionView.delegate = self collectionView.register(ZodiacCardCell.self, forCellWithReuseIdentifier: "ZodiacCardCell") collectionView.showsHorizontalScrollIndicator = false collectionView.isPagingEnabled = false // 关闭分页,用自定义对齐逻辑 collectionView.decelerationRate = .fast view.addSubview(collectionView) } }
1.2 添加中心金色圆圈指示器
private func setupCenterIndicator() { let indicatorSize: CGFloat = 140 let indicatorView = UIView(frame: CGRect(x: (view.bounds.width - indicatorSize)/2, y: view.bounds.height/2 - indicatorSize/2, width: indicatorSize, height: indicatorSize)) indicatorView.layer.cornerRadius = indicatorSize/2 indicatorView.layer.borderWidth = 3 indicatorView.layer.borderColor = UIColor.systemYellow.cgColor // 金色边框 indicatorView.backgroundColor = .clear view.addSubview(indicatorView) view.bringSubviewToFront(indicatorView) // 确保指示器在CollectionView上方 }
2. 自定义卡片Cell
class ZodiacCardCell: UICollectionViewCell { private let imageView = UIImageView() override init(frame: CGRect) { super.init(frame: frame) setupImageView() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupImageView() { imageView.frame = bounds imageView.contentMode = .scaleAspectFit addSubview(imageView) } func configure(with imageName: String) { imageView.image = UIImage(named: imageName) } }
3. 滚动自动对齐与透明度控制
3.1 实现滚动停止后自动居中
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { alignCollectionViewToCenter() } } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { alignCollectionViewToCenter() } private func alignCollectionViewToCenter() { let centerX = collectionView.contentOffset.x + collectionView.bounds.width/2 let closestIndex = collectionView.visibleCells.compactMap { cell in collectionView.indexPath(for: cell)?.item }.min { index1, index2 in let frame1 = collectionView.layoutAttributesForItem(at: IndexPath(item: index1, section: 0))?.frame ?? .zero let frame2 = collectionView.layoutAttributesForItem(at: IndexPath(item: index2, section: 0))?.frame ?? .zero return abs(frame1.midX - centerX) < abs(frame2.midX - centerX) } ?? 0 let targetIndexPath = IndexPath(item: closestIndex, section: 0) collectionView.scrollToItem(at: targetIndexPath, at: .centeredHorizontally, animated: true) selectedZodiacIndex = closestIndex updateSelectedZodiac() // 这里更新选中后的业务逻辑,比如通知上层或更新UI } private func updateSelectedZodiac() { let selectedZodiac = zodiacImages[selectedZodiacIndex] // 处理选中后的操作,比如显示选中的星座名称等 print("当前选中星座:\(selectedZodiac)") }
3.2 动态调整卡片透明度
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { updateCellAlpha(cell: cell) } func scrollViewDidScroll(_ scrollView: UIScrollView) { collectionView.visibleCells.forEach { cell in updateCellAlpha(cell: cell) } } private func updateCellAlpha(cell: UICollectionViewCell) { let cellCenterX = cell.center.x let collectionCenterX = collectionView.bounds.midX + collectionView.contentOffset.x let distance = abs(cellCenterX - collectionCenterX) // 当卡片中心与CollectionView中心的距离超过20(可调整)时,设置透明度0.5,否则1 cell.alpha = distance > 20 ? 0.5 : 1.0 }
4. CollectionView数据源实现
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return zodiacImages.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ZodiacCardCell", for: indexPath) as! ZodiacCardCell cell.configure(with: zodiacImages[indexPath.item]) return cell }
关键点说明
sectionInset的设置确保初始状态下第一个卡片居中,左右边距等于(屏幕宽度-卡片宽度)/2- 滚动对齐逻辑通过计算可见卡片与中心的距离,找到最近的卡片并滚动到居中位置
- 透明度调整在滚动过程中实时更新,确保只有中心区域的卡片保持完全不透明
内容的提问来源于stack exchange,提问作者RinaredStaring
相关产品推荐
相关产品推荐

