iOS UITableView+UICollectionView嵌套,手势冲突解决

首先,新的一年,怀有期待的努力着!这两个月,昼夜不分. 看着自己写的代码,才意识到日子是切切实实的过了的.
这个主题我很早很早就想写的. 然后就没有然后了. 这次开发中正好有这个需求,那当然是不能再错过的. 主要记录实现的原理以及遇到的问题和解决方案.

1. 页面展示

定义:

  • MainTableView: 就是用户看到的滚动视图,mainTableView的父视图就是self.view.
  • ContentTableViewCell:就是tableView的cell,和普通的自定义cell一样.这个里面放的就是我们的HeaderView,当然你也可以用tableHeaderView来实现.Cell有一个好处就是自适应内容的高度,并且很容易的取到这个高度.
  • ContentView:这个是ContentTableViewCell的真实的ContentView,这就是一个简单的UIView,但里面的子视图是一个UICollectionView.
  • TitleView:这就是items的View,这里把这个View作为TableView的sectionHeaderView来实现.
  • ContentCollectionView:最上层用来展示数据的collectionView,根据需求用collectionView还是tableView,一般来说都是一个列表.

相信写到这里,你已经知道了大概的实现原理了吧. 实现的最终效果如下.


output.gif

2. 实现思路

实现这个页面,最重要的问题是要解决手势冲突.监听MainTableView和contentCollectionView的滚动方法来控制彼此的ContentOffset. MainTableView和contentCollectionView之间的通信有多种方式.通知或者代理.这里采用的是代理的方法,用代理,在同一个VC里实现滚动的监听,看起来更清楚一些.

2.1 实现下面方法满足同时监听多个相同手势

相同类型的手势,同一时间只有一个能够得到辨认.下面这个方法返回Yes也就意味着所有相同类型的手势都能得到处理.

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    return YES;
}

2.2 MainTableView的滚动监听

临界点就是需要悬停的位置.也就是HeaderView刚好消失的位置.这里用的Cell,就可以直接用 bottomCellOffset = [_contentTableView rectForSection:1].origin.y取到Header的高度.如果bottomCellOffset > offSetY,说明到了顶处,这个时候就固定MainTableView的contentOffSet,让MainTableView保持不动.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {   
    CGFloat bottomCellOffset = [_contentTableView rectForSection:1].origin.y;
    if (self.currentScrollingListView != nil && self.currentScrollingListView.contentOffset.y > 0) {
        //mainTableView的header已经滚动不见,开始滚动某一个listView,那么固定mainTableView的contentOffset,让其不动.
        _contentTableView.contentOffset = CGPointMake(0, bottomCellOffset);
    }
    //mainTableView已经显示了header,listView的contentOffset需要重置.
    if (scrollView.contentOffset.y < bottomCellOffset) {
        if(_currentScrollingListView.contentOffset.y > 0) {
            _currentScrollingListView.contentOffset = CGPointZero;
        }
    }
}

2.3 contentCollectionView的滚动监听

这里用的是代理.所以这个方法也是在MainViewController里.记录当前滚动的ListView.通过与lastScrollingListViewContentOffsetY的比较可以判断contentCollectionView是向上滚动,还是向下滚动.这里有一个问题:需要设置MainTableView的bouces = NO,如果没有设置这个的话,那么_contentTableView.contentOffset.y == 0几乎就不会执行.但是这个设置为NO的同时也会引发另一个问题,这个在下面的问题中列出.

其实简单的说就是以HeaderView的高度为临界点,在HeaderView消失之前,就让MainTableView滚动,而让contentCollectionView固定不动.反之,就让contentCollectionView滚动,MainTableView固定不动.

- (void)contentCollectionViewDidScroll:(UICollectionView *)contentCollectionView
{
    self.currentScrollingListView = contentCollectionView;
    
    CGFloat bottomCellOffset = [_contentTableView rectForSection:1].origin.y;
    BOOL shouldProcess = YES;
    
    if (contentCollectionView.contentOffset.y > self.lastScrollingListViewContentOffsetY) {
    }
    else {
        if(_contentTableView.contentOffset.y == 0) shouldProcess = NO;
        else {
            if(_contentTableView.contentOffset.y < bottomCellOffset) {
                _currentScrollingListView.contentOffset = CGPointZero;
            }
        }
    }
    if (shouldProcess) {
        if (_contentTableView.contentOffset.y < bottomCellOffset) {
            if (_currentScrollingListView.contentOffset.y > 0) {
                _currentScrollingListView.contentOffset = CGPointZero;
            }
        }
        else {
            _contentTableView.contentOffset = CGPointMake(0, bottomCellOffset);
        }
    }
    self.lastScrollingListViewContentOffsetY = self.currentScrollingListView.contentOffset.y;  
}

另外,关于CollectionView的定点问题.有时间我再整理一下.

[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:contentViewCurrentIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionNone animated:NO];

3. 一些问题和解决方案

3.1 当contentCollectionView没有数据源时,滚动到悬停位置后无法滑动到原来的状态?

当列表没有数据时就检测不到滑动手势。可以根据需要,或者在网络请求的回调中做判断,如果没有数据或者网络请求失败等情况添加contentCollectionView的noDataView.

3.2 上滑之后会出现第一次点击cell不响应点击事件???

初始化tableView的时候添加以下代码.cancelsTouchesInView设置为NO,当两个事件有冲突时,都会响应两个事件.所以这个设置为NO可以解决很多的问题.

_contentTableView.panGestureRecognizer.cancelsTouchesInView = NO; 

3.3 如何动态显示titleView item的个数?

这个在网络请求回调中初始化TitleArray就可以了.这个有什么问题呢?

3.4 关于刷新?

其实还是上面关于临界点的问题.如何做到下拉的时候MainTableView固定不动,而contentCollectionView可以滚动.所以这个地方,就必须要设置MainTableView的bonces.上面的代码就已经可以实现了.

3.5 mainTableView.bounces = NO;设置后滚动到一定位置后无法滚动?

一般我们都是通过懒加载的方式去创建控件,如果在这个时候去设置frame,那么这个frame一经设置是不会改变的.所以需要在下面这个方法里面设置tableView的frame.viewDidLayoutSubviews方法是当Controller的子视图的positon和frame发生改变时会被调用.也就是执行完AutoLayout后会调用这个方法.

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    self.contentTableView.frame = self.view.bounds;
}

3.6 滚动的时候,会重新调用viewDidLoad方法?(这个是网上的一个Demo,之前我也有参考)

如果你也遇到同样的问题. 试试不要用懒加载的方式去创建MainTableView.这里的问题是因为在监听滚动的时候,vc.contentCollectionView.contentOffset会导致subView被添加到self.view上从而引发了viewDidLoad方法的调用.具体的原因还需要深度剖析.但不通过懒加载可以解决这个问题.

    for (id  vc in _viewControllers) {
            vc.vcCanScroll = cellCanScroll;
            if (!cellCanScroll) {
                vc.contentCollectionView.contentOffset = CGPointZero;
            }
        }

4. 巨人的肩膀

嵌套滚动参考一
嵌套滚动参考二

5. 总结

以上,就是关于tableView的嵌套使用.基本上遇到的问题我都列出来了,后续如果我再发现其它的问题也会补上,最近有不少想要总结的点,但真的太忙了,一有时间就会更新的。终于完成了这一篇博客!2019!

推荐阅读更多精彩内容