Swift 4中如何实现ScrollView与CollectionView联动滚动?
解决ScrollView与CollectionView同步滚动的Swift 4实现方案
嘿,这个问题我做项目时也碰到过!你现在的核心问题其实有两个:一是铺满视图后UIScrollView的contentSize没有被正确计算,导致滚动功能直接失效;二是嵌套ScrollView和CollectionView时的滚动冲突问题。下面给你两个靠谱的实现方案,都是Swift 4里能用的:
方案一:用UICollectionView作为主容器(推荐)
既然要整个页面同步滚动,完全不需要嵌套UIScrollView——因为UICollectionView本身就是UIScrollView的子类,天生支持滚动,而且性能更优。你可以把页面里的非列表内容(比如顶部标题栏、操作按钮等)做成CollectionView的HeaderView,列表部分就是常规Cell,这样整个页面就是一个CollectionView,自然能同步滚动,完全不会有冲突。
简单代码示例:
- 注册Header和Cell:
override func viewDidLoad() { super.viewDidLoad() // 注册Header collectionView.register(TopHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderID") // 注册Cell collectionView.register(ListCell.self, forCellWithReuseIdentifier: "CellID") // 设置FlowLayout if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout { layout.headerReferenceSize = CGSize(width: view.frame.width, height: 200) // 自定义Header高度 layout.itemSize = CGSize(width: view.frame.width, height: 80) // 自定义Cell高度 } }
- 实现Header和Cell的数据源方法:
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "HeaderID", for: indexPath) as! TopHeaderView // 配置Header内容 header.titleLabel.text = "我的页面标题" return header } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CellID", for: indexPath) as! ListCell // 配置Cell内容 cell.contentLabel.text = "列表项 \(indexPath.row)" return cell }
方案二:正确配置UIScrollView嵌套CollectionView(如果必须用ScrollView)
如果你因为某些原因必须保留外层UIScrollView,那关键是要让ScrollView能正确计算contentSize,同时禁用CollectionView的滚动,让外层ScrollView统一控制整个页面的滚动:
步骤:
给ScrollView添加内容容器:
不要直接把CollectionView和其他控件放在ScrollView里,先给ScrollView添加一个UIView作为contentView,设置contentView的约束:- top、leading、trailing、bottom 都和ScrollView的对应边缘对齐
- contentView的width等于父视图(控制器view)的width(避免横向滚动)
- contentView的height不需要固定,让内部控件自动撑开
配置CollectionView:
- 把CollectionView放在contentView里,设置它的top、leading、trailing和contentView对齐
- 禁用CollectionView的滚动:
collectionView.isScrollEnabled = false - 给CollectionView添加一个height约束,然后监听它的
contentSize变化,动态更新这个高度约束,这样ScrollView就能识别到整个内容的高度
代码示例:
// 先定义CollectionView的高度约束 var collectionViewHeightConstraint: NSLayoutConstraint! override func viewDidLoad() { super.viewDidLoad() // 初始化contentView并添加到ScrollView let contentView = UIView() contentView.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview(contentView) // 设置contentView约束 NSLayoutConstraint.activate([ contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), contentView.widthAnchor.constraint(equalTo: view.widthAnchor) ]) // 初始化CollectionView let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.isScrollEnabled = false contentView.addSubview(collectionView) // 设置CollectionView约束 collectionViewHeightConstraint = collectionView.heightAnchor.constraint(equalToConstant: 0) NSLayoutConstraint.activate([ collectionView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20), collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), collectionViewHeightConstraint ]) // 监听CollectionView的contentSize变化 collectionView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil) } // 处理contentSize变化 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == "contentSize" { guard let collectionView = object as? UICollectionView else { return } collectionViewHeightConstraint.constant = collectionView.contentSize.height } } // 别忘了移除观察者 deinit { collectionView.removeObserver(self, forKeyPath: "contentSize") }
注意点:
- 千万不要直接给ScrollView设置固定的contentSize,最好通过自动布局让contentView撑开,这样适配不同屏幕更灵活
- 嵌套滚动时一定要禁用内层CollectionView的滚动,否则会出现两个滚动视图抢触摸事件的情况
内容的提问来源于stack exchange,提问作者Rafaela Lourenço
相关产品推荐
相关产品推荐

