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

基于UICollectionView的自定义菜单栏联动滑动问题求助

解决UICollectionView菜单栏与内容视图联动问题

我看了你的代码,核心问题是:你在滑动内容视图时给segmentedViews.selectedMenu赋了值,但SegmentedView里没有监听这个属性的变化,自然不会触发菜单栏的滚动和选中状态更新。另外还有几个细节需要调整,咱们一步步来修复:

1. 给菜单栏的selectedMenu添加属性监听

在SegmentedView里,给selectedMenu加上didSet观察器,一旦值变化就自动更新菜单栏的滚动位置和选中项状态:

var selectedMenu: CGFloat? {
    didSet {
        guard let selectedMenu = selectedMenu else { return }
        // 计算每个菜单item的宽度(均分菜单栏宽度)
        let itemWidth = frame.width / CGFloat(knowledge.count)
        // 计算目标滚动偏移量
        let targetOffset = CGPoint(x: selectedMenu * itemWidth, y: 0)
        // 平滑滚动到对应位置
        collectionView.setContentOffset(targetOffset, animated: true)
        // 更新选中item的UI状态
        updateSelectedItem()
    }
}

2. 确保菜单item宽度正确设置

在SegmentedView的UICollectionViewDelegateFlowLayout扩展里,设置每个item的宽度为菜单栏总宽度的1/3,保证和内容视图的每页宽度对应:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CGSize(width: frame.width / CGFloat(knowledge.count), height: frame.height)
}

3. 优化内容视图的滚动偏移计算

在SegmentedViewController的scrollViewDidScroll里,用内容视图的偏移量除以自身宽度,得到当前页面的索引(滑动过程中是连续值,支持平滑联动):

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    // 计算当前页面位置(0=第一页,1=第二页,依此类推)
    let currentPage = scrollView.contentOffset.x / scrollView.frame.width
    segmentedViews.selectedMenu = currentPage
}

如果只需要分页结束后再联动,可以改用scrollViewDidEndDecelerating方法,避免滑动过程中的频繁更新。

4. 实现菜单item的选中状态同步

给SegmentedCell添加isSelected的状态处理,比如改变文字颜色、添加下划线:

override var isSelected: Bool {
    didSet {
        nameLabel.textColor = isSelected ? .systemBlue : .darkGray
        // 添加/移除选中下划线
        if isSelected {
            let underline = UIView()
            underline.backgroundColor = .systemBlue
            underline.translatesAutoresizingMaskIntoConstraints = false
            addSubview(underline)
            NSLayoutConstraint.activate([
                underline.bottomAnchor.constraint(equalTo: bottomAnchor),
                underline.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor),
                underline.trailingAnchor.constraint(equalTo: nameLabel.trailingAnchor),
                underline.heightAnchor.constraint(equalToConstant: 2)
            ])
        } else {
            subviews.first(where: { $0.backgroundColor == .systemBlue })?.removeFromSuperview()
        }
    }
}

5. 双向联动(可选)

如果需要点击菜单时也能切换内容视图,在SegmentedView的didSelectItemAt里添加反向触发逻辑:

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    selectedMenu = CGFloat(indexPath.item)
    // 找到父视图控制器,触发内容视图滚动
    if let parentVC = superview?.next as? SegmentedViewController {
        let targetOffset = CGPoint(x: CGFloat(indexPath.item) * parentVC.collectionView.frame.width, y: 0)
        parentVC.collectionView.setContentOffset(targetOffset, animated: true)
    }
}

完整修改后的代码

SegmentedView 部分

class SegmentedView: UIView {
    let collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        layout.minimumInteritemSpacing = 0
        layout.minimumLineSpacing = 0
        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.showsHorizontalScrollIndicator = false
        cv.isScrollEnabled = true
        return cv
    }()
    let knowledge = ["Pengetahuan", "Keterampilan", "Sikap"]
    var selectedMenu: CGFloat? {
        didSet {
            guard let selectedMenu = selectedMenu else { return }
            let itemWidth = frame.width / CGFloat(knowledge.count)
            let targetOffset = CGPoint(x: selectedMenu * itemWidth, y: 0)
            collectionView.setContentOffset(targetOffset, animated: true)
            updateSelectedItem()
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupView()
    }
    
    private func setupView() {
        addSubview(collectionView)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
        
        collectionView.register(SegmentedCell.self, forCellWithReuseIdentifier: "cellId")
        collectionView.dataSource = self
        collectionView.delegate = self
    }
    
    private func updateSelectedItem() {
        guard let selectedMenu = selectedMenu else { return }
        let currentIndex = Int(selectedMenu.rounded())
        for cell in collectionView.visibleCells {
            guard let segmentedCell = cell as? SegmentedCell else { continue }
            let indexPath = collectionView.indexPath(for: cell)!
            segmentedCell.isSelected = indexPath.item == currentIndex
        }
    }
}

extension SegmentedView: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return knowledge.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! SegmentedCell
        let item = knowledge[indexPath.item]
        cell.nameLabel.text = item
        cell.isSelected = indexPath.item == Int(selectedMenu ?? 0)
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: frame.width / CGFloat(knowledge.count), height: frame.height)
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        selectedMenu = CGFloat(indexPath.item)
        if let parentVC = superview?.next as? SegmentedViewController {
            let targetOffset = CGPoint(x: CGFloat(indexPath.item) * parentVC.collectionView.frame.width, y: 0)
            parentVC.collectionView.setContentOffset(targetOffset, animated: true)
        }
    }
}

SegmentedCell 部分

class SegmentedCell: UICollectionViewCell {
    let nameLabel: UILabel = {
        let label = UILabel()
        label.textAlignment = .center
        label.font = UIFont.systemFont(ofSize: 16)
        return label
    }()
    
    override var isSelected: Bool {
        didSet {
            nameLabel.textColor = isSelected ? .systemBlue : .darkGray
            if isSelected {
                let underline = UIView()
                underline.backgroundColor = .systemBlue
                underline.translatesAutoresizingMaskIntoConstraints = false
                addSubview(underline)
                NSLayoutConstraint.activate([
                    underline.bottomAnchor.constraint(equalTo: bottomAnchor),
                    underline.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor),
                    underline.trailingAnchor.constraint(equalTo: nameLabel.trailingAnchor),
                    underline.heightAnchor.constraint(equalToConstant: 2)
                ])
            } else {
                subviews.first(where: { $0.backgroundColor == .systemBlue })?.removeFromSuperview()
            }
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupCell()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupCell()
    }
    
    private func setupCell() {
        addSubview(nameLabel)
        nameLabel.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            nameLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
            nameLabel.centerYAnchor.constraint(equalTo: centerYAnchor)
        ])
    }
}

SegmentedViewController 部分

class SegmentedViewController: UIViewController {
    @IBOutlet weak var collectionView: UICollectionView!
    @IBOutlet weak var segmentedViews: SegmentedView!
    let cellId = "assesmentCell"
    let colors: [UIColor] = [UIColor.blue, UIColor.yellow, UIColor.green]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupCollection()
        segmentedViews.selectedMenu = 0
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let currentPage = scrollView.contentOffset.x / scrollView.frame.width
        segmentedViews.selectedMenu = currentPage
    }
    
    func setupCollection() {
        if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
            flowLayout.scrollDirection = .horizontal
            flowLayout.minimumLineSpacing = 0
            flowLayout.itemSize = CGSize(width: view.frame.width, height: collectionView.frame.height)
        }
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellId)
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.isPagingEnabled = true
        collectionView.showsHorizontalScrollIndicator = false
    }
}

extension SegmentedViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return colors.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath)
        cell.backgroundColor = colors[indexPath.item]
        return cell
    }
}

extension SegmentedViewController: UICollectionViewDelegate {
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        let currentPage = scrollView.contentOffset.x / scrollView.frame.width
        segmentedViews.selectedMenu = currentPage
    }
}

这样修改后,滑动内容视图时菜单栏会平滑联动滚动,点击菜单也能切换内容视图,选中状态也会同步更新。

内容的提问来源于stack exchange,提问作者Ferry Adi Wijayanto

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.06 10:52:50