基于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
相关产品推荐
相关产品推荐

