将UICollectionView嵌入UIScrollView后滚动衔接问题排查
解决嵌套UIScrollView与UICollectionView的无缝滚动切换问题
兄弟,我太懂这种嵌套滚动卡壳的难受了——明明逻辑上应该无缝切换滚动对象,结果就是卡在边界上动不了,要么回弹要么得刻意点击才行。结合你说的问题,核心确实是触摸事件的传递和滚动优先级没处理好,cancelContentTouches只是其中一个小环节,咱们一步步来搞定:
核心问题分析
出现这两个问题的本质原因是系统的触摸事件分发逻辑:
- 当UICollectionView滚到顶部时,它会优先消耗触摸事件,哪怕已经没有内容可滚,还是会触发回弹,不会把事件传递给父UIScrollView
- 父UIScrollView滚到底部时,同样会优先处理自身的回弹逻辑,不会把后续的滚动事件交给子CollectionView
解决方案代码实现
咱们从配置属性、代理拦截、手势判断三个维度入手:
1. 先搞定基础滚动属性配置
先把两个滚动视图的触摸相关属性设置对,这是基础:
// 配置父UIScrollView scrollView.canCancelContentTouches = true scrollView.delaysContentTouches = false // 配置子UICollectionView collectionView.canCancelContentTouches = true collectionView.delaysContentTouches = false
2. 通过代理方法拦截滚动切换时机
让你的MainViewController同时成为父ScrollView和子CollectionView的代理,然后实现以下代理方法:
(1)滚动过程中动态切换滚动对象
func scrollViewDidScroll(_ scrollView: UIScrollView) { // 处理从CollectionView切换到父ScrollView(向上滚动到顶) if scrollView == collectionView { if collectionView.contentOffset.y <= 0 { // 锁定CollectionView在顶部,交给父ScrollView处理滚动 collectionView.contentOffset = .zero collectionView.isScrollEnabled = false self.scrollView.isScrollEnabled = true } } // 处理从父ScrollView切换到CollectionView(向下滚动到底) else if scrollView == self.scrollView { // 留10的容错值,避免浮点精度问题导致误判 let isScrollViewAtBottom = scrollView.contentOffset.y + scrollView.frame.height >= scrollView.contentSize.height - 10 if isScrollViewAtBottom { // 锁定父ScrollView在底部,交给CollectionView处理滚动 scrollView.isScrollEnabled = false collectionView.isScrollEnabled = true } } }
(2)触摸开始时提前判断滚动对象
解决多次轻扫只有回弹的问题,在触摸开始时就判断该由谁接管:
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { if scrollView == collectionView { // 若CollectionView已在顶部,直接让父ScrollView接管 if collectionView.contentOffset.y <= 0 { collectionView.isScrollEnabled = false self.scrollView.isScrollEnabled = true // 手动触发父ScrollView的手势,确保事件传递过去 self.scrollView.panGestureRecognizer.state = .began } } else if scrollView == self.scrollView { // 若父ScrollView已在底部,直接让CollectionView接管 let isAtBottom = scrollView.contentOffset.y + scrollView.frame.height >= scrollView.contentSize.height if isAtBottom { scrollView.isScrollEnabled = false collectionView.isScrollEnabled = true collectionView.panGestureRecognizer.state = .began } } }
(3)滚动结束后恢复可滚动状态
避免下次滚动时被锁定,滚动停止后恢复状态:
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if scrollView == collectionView { // 只要CollectionView不在顶部,就恢复父ScrollView的滚动权限 if collectionView.contentOffset.y > 0 { self.scrollView.isScrollEnabled = true } } else if scrollView == self.scrollView { // 只要父ScrollView不在底部,就恢复CollectionView的滚动权限 let isNotAtBottom = scrollView.contentOffset.y + scrollView.frame.height < scrollView.contentSize.height - 10 if isNotAtBottom { collectionView.isScrollEnabled = true } } }
3. 拦截CollectionView的顶部回弹手势
最后解决多次轻扫只有回弹的问题,通过手势代理拦截不必要的手势响应:
先给ViewController添加UIGestureRecognizerDelegate扩展:
extension MainViewController: UIGestureRecognizerDelegate { func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { guard gestureRecognizer == collectionView.panGestureRecognizer else { return true } // 当CollectionView在顶部,且用户手势是向上滑动时,不让CollectionView的手势响应 let translation = collectionView.panGestureRecognizer.translation(in: collectionView) // 注意:手指向上滑,translation.y为负值 if collectionView.contentOffset.y <= 0 && translation.y < 0 { return false } return true } }
然后记得设置手势代理:
collectionView.panGestureRecognizer.delegate = self
额外注意事项
- 你的视图层级里的
CollectionViewContainerView如果是滚动视图的话,一定要把它的滚动禁用掉,避免干扰事件传递 - 计算底部阈值时的容错值可以根据你的布局调整,确保判断准确
- 如果需要保留回弹效果,可以在代理方法里微调逻辑,比如等回弹动画结束后再切换滚动对象,但可能会稍微影响流畅度
按照这套逻辑调整后,应该就能实现你想要的无缝滚动切换了——向上滑到CollectionView顶部自动切到父ScrollView,向下滑到父ScrollView底部自动切到CollectionView,再也不用刻意点击啦。
内容的提问来源于stack exchange,提问作者Emin Israfil iOS
相关产品推荐
相关产品推荐

