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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 16:52:47