美文网首页iOS DeveloperiOS开发iOS 知识点
iOS 多级 UIScrollView 嵌套的实现方案

iOS 多级 UIScrollView 嵌套的实现方案

作者: 雷曼同学 | 来源:发表于2018-05-06 01:56 被阅读1473次

    本文实现了一种多级 UIScrollView 嵌套的交互,主要解决事件传递和手势冲突问题。

    一、效果展示

    首先来直观地看一下要实现的效果。在实现过程中,代码做到了尽可能的解耦,将页面的各个部分拆离,单独封装成控件。

    二、布局方式

    首先来参照下面的图片,看看页面的每个部分对应哪个控件。

    结合上面的动画,可以看到,MFNestTableView 部分可以上下滑动, MFPageView 部分可以左右滑动, 当 MFSegmentView 固定到顶部后, MFPageView 的内容部分可以单独上下滑动。

    三、实现原理

    1. MFNestTableView

    基于 UITableView 来实现。

    注意: UITableView 也是一种 UIScrollView ,下面用 UIScrollView 来统称。

    关键点一:允许两个嵌套的 UIScrollView 同时响应触摸事件,即两个 UIScrollView 可以同时滚动。

    // 返回YES表示可以继续传递触摸事件,这样两个嵌套的scrollView才能同时滚动
    - (BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
        id view = otherGestureRecognizer.view;
        if ([[view superview] isKindOfClass:[UIWebView class]]) {
            view = [view superview];
        }
        
        if (_allowGestureEventPassViews && [_allowGestureEventPassViews containsObject:view]) {
            return YES;
        } else {
            return NO;
        }
    }
    

    这里判断是允许同时响应的 View ,才返回 YES

    关键点二:在同一时间只允许一个 UIScrollView 产生偏移,另一个通过 contentOffset 来固定位置。然后在合适的时候进行切换,这样实现的效果是,即使两层 UIScrollView 都能响应触摸事件,但不会发生同时滚动。

    父级的 UIScrollView

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
        
        CGFloat contentOffset = [self heightForContainerCanScroll];
        
        if (!_canScroll) {
            // 这里通过固定contentOffset的值,来实现不滚动
            scrollView.contentOffset = CGPointMake(0, contentOffset);
        } else if (scrollView.contentOffset.y >= contentOffset) {
            scrollView.contentOffset = CGPointMake(0, contentOffset);
            self.canScroll = NO;
            
            // 通知delegate内容开始可以滚动
            if (self.delegate && [self.delegate respondsToSelector:@selector(nestTableViewContentCanScroll:)]) {
                [self.delegate nestTableViewContentCanScroll:self];
            }
        }
        scrollView.showsVerticalScrollIndicator = _canScroll;
        
        if (self.delegate && [self.delegate respondsToSelector:@selector(nestTableViewDidScroll:)]) {
            [self.delegate nestTableViewDidScroll:_tableView];
        }
    }
    

    子级的 UIScrollView

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
        
        if (!_canContentScroll) {
            // 这里通过固定contentOffset,来实现不滚动
            scrollView.contentOffset = CGPointZero;
        } else if (scrollView.contentOffset.y <= 0) {
            _canContentScroll = NO;
            // 通知容器可以开始滚动
            _nestTableView.canScroll = YES;
        }
        scrollView.showsVerticalScrollIndicator = _canContentScroll;
    }
    

    这里滚动条的显示隐藏也做了切换,即谁当前可以滚动,就显示谁的滚动条。

    2. MFSegmentView

    基于 UICollectionView 实现,通过 scrollToItemAtIndexPath:atScrollPosition:animated: 来实现点击后滑动到对应位置的效果。

    3. MFPageView

    基于 UICollectionView 实现, MFPageViewView 可以是 UITableViewUIScrollViewUICollectionViewUIWebView 。总之只要能响应 scrollViewDidScroll: 就可以。

    其中对于 UIScrollView ,注意设置 scrollView.alwaysBounceVertical = YES; ,这样当 contentSize 小于 frame.size 时, scrollView 也可以滚动。

    四、适配

    因为当前哪个 UIScrollView 可以滚动,是根据 contentOffset 计算后决定的,而 UIScrollView 初始化后的 contentOffset 却有多种情况,会受到 navigationBar.translucent 这个值的影响。

    MFNestTableView 控件默认采取的方案是 navigationBar 不透明的情况,此时初始化后的 contentOffset 为 0。而 Demo 采取的是 navigationBar.translucent == YES 的情况, 在这种情况下, iPhone X 和其他机型的初始 contentOffset 值也不一样,因为 navigationBar 的高度不一样。这个时候需要实现 MFNestTableViewDataSourcenestTableViewContentInsetTop: 方法。

    - (CGFloat)nestTableViewContentInsetTop:(MFNestTableView *)nestTableView {
        
        // 因为这里navigationBar.translucent == YES,所以实现这个方法,返回下面的值
        if (IS_IPHONE_X) {
            return 88;
        } else {
            return 64;
        }
    }
    

    源码

    请到 GitHub 上查看完整例子。

    参考

    iOS scrollView嵌套tableView的手势冲突解决方案

    获取更佳的阅读体验,请访问原文地址 【Lyman's Blog】iOS 多级 UIScrollView 嵌套的实现方案

    相关文章

      网友评论

      • helloJJY:你好,我在nestTableView添加UICollectionView,滑动UICollectionView,并不会走控制器的- (void)scrollViewDidScroll:(UIScrollView *)scrollView 方法,当MFSegmentView滑动到顶端位置的时候,MFSegmentView不能往下滑动。请问下有没有解决方法
        helloJJY:@雷曼同学 厉害了同学,可以了
        雷曼同学:试试设置 collectView.alwaysBounceVertical = YES;
      • 5c6d09145665:子视图加下拉刷新要怎么搞....:confounded:
        一根聪:@s超s 试试我的这个https://www.jianshu.com/p/4ba423799018
        5c6d09145665:我是用contentoffset 做的
        s超s:搞出来了吗,怎么搞
      • 小古要哈哈:https://www.jianshu.com/p/5b28a0a11f60 这个和你的也差不多,做起来超级赞的
      • BobooO:大兄弟这个实现思路跟我写的HQInlineListView有点类似 https://github.com/debolee/HQInlineListView.git,不过我将MFNestTableView-header部分用一个section实现,可以自定义cell内容和数量、另外也支持自动布局、支持通过xib创建。
      • e8f76c0d43fa:你好,现在遇到这样一个情况。
        主控制器的nestTableView的内容页中加入另外一个控制器的tableView,主控制器不代理该tableView的delegate时,nestTableView上移(即segmentView悬停)之后,无法让nestTableView下移,只能移动内部的tableView。
        若主控制器代理tableView的delegate,则tableView的delegate相关的方法失效(如组头组尾),滑动正常。
        这种情况有没有两全的解决方法呢?
        雷曼同学:@AlHoo 确实会有你说的代码臃肿问题,单纯从解耦的角度来说,目前能想到的还有从 category 方向入手。
        e8f76c0d43fa:@雷曼同学 你好,感谢回复。如你所说是在nestTableView中放置了其他的tableView,至于放在另一个controller中,是因为想要解耦且页面需要复用。还有一个原因是如果将多个tableView的处理和代理实现放置在nestTableView的控制器中,会非常臃肿和深耦合,复用也比较困难。
        目前想到的解决办法是其他的tableView的控制器封装相应的代理实现方法供nestTableView的控制器调用,暂时没有想到其他更好的办法,只能这样尽量解耦和复用了。
        期待更多交流。
        雷曼同学:@AlHoo 你的意思是在nestTableView中,放入另一个controller中的tableView吗?为什么要把tableView放在另一个controller中呢?
      • 80c9a4518c6f:你好,我看你的demo,翻译了一遍swift版本的,我可以发布吗
        d4de4177e332:请问能给我看看嘛
        雷曼同学:@SkyerWalker 可以呀~
      • 807baf846a3c:给contentView上的tableview加了下拉刷新,没办法拉下来怎么办?
        5c6d09145665:搞出来了么?怎么处理contentOffset?
        巩固2022:怎么解决的呢?谢谢
        807baf846a3c:就像demo里的列表1和列表2那种,怎样去加下拉刷新和上拉加载呢?
      • 你的可爱猪队友:大兄dei,当tableViewCell数据源不够填满tableView(未超出屏幕范围时),不能够下拉阿...也就是只有一两个数据源的时候,就一直定在上面了,这个咋整噢?望回复
        雷曼同学:@JFPURE 不能往下拉的根本原因是触发不了子tableView 的 scrollViewDidScroll: 方法,你看看是不是因为子tableView 的 frame 没有撑满的原因。我相应的处理是在 MFPageView.m 的 layoutSubviews 方法和 collectionView: cellForItemAtIndexPath: 方法,你看看是不是这个问题。
        你的可爱猪队友:@雷曼同学 我是套用了你的Demo,然后每个列表是自己实现的。列表里是collectionView 有区别的吗?
        雷曼同学:请问你是用我的 demo 吗,还是说你自己实现的?在我的 demo 中,列表1的数据就只有两行,但是是可以下拉的。
      • 路小白同学:你好,请问如何实现滑动_mainTableView 到顶之后下面的TableView自动网上滑动呢?
        路小白同学:@小白路先生 滑动
        路小白同学:@雷曼同学 如果说点击焦点是在headImageView把_mainTableView 滑动到顶端停顿,那么下面的View就会停止,因为没有点击到它们,有没有方法让_mainTableView停止后下面的开始华东东呢?
        雷曼同学:不太理解你说的自动往上滑动是什么意思,可以说详细一点吗?
      • onetwo_a:不能保存滑动进度是个问题,比如把页面3滑倒最底部,这个时候表头是在顶部的,然后切到列表2把表头拉下来再切到列表3,它的偏移量也已经变成0了。
        虽然nestTableViewContainerCanScroll:里面的代码规避了切换页面之后才变0,但保存页面滑动进度确实也是硬需求啊,希望作者可以稍微改进一下。
        onetwo_a:@雷曼同学 不能保存页面进度意味着每次进入这个页面都要从头滑动,如果这个页面很长,那可能会影响体验。
        onetwo_a:@雷曼同学 简书的用户页的确是这样的,不过就我手机上面装的应用来说,类似的结构:headerview+标签栏+并列横滚页面,很多都是可以保存列表页面的滑动进度的,比如豆瓣小组页面,喜马拉雅,外卖app的店铺页面,b站视频播放页面等等。
        雷曼同学:一般这种类似的交互,在header拉下来之后,再切换其他页面,都是要求偏移量归0的吧(可以参照一下简书APP的用户主页)。
        另外,假如不归0,就会出现header还停留在屏幕内,但内容列表已经产生偏移的现象。你应该也不是想要这种效果吧。
      • Calabash_Boy:思路很清晰,不过footer采用tableViewFooter会不会更好些,这样的话原来需要计算的sectionFooter的高度现在都不用计算了,而且定制化也更好些,大佬觉得呢?
        雷曼同学:@Calabash_Boy 客气了,互相学习:smile:
        Calabash_Boy:@雷曼同学 嗯,的确是这样,我的业务需求是在tableView的底部增加了广告区域,拉到底部才能看到,不需要固定在屏幕底部,但是在过渡的时候有些许卡顿,现在打算重写这个界面,大佬的这个封装很给力,而且可扩展的地方也很多,给大佬递茶.
        雷曼同学:这里用 sectionFooter 是考虑到一个固定底部的问题,因为 tableViewFooter 是需要拉到最底部才能看到,而 sectionFooter 是可以一直固定在屏幕的底部,考虑到多数的需求还是属于后者(比如我们的产品),所以做了这样的设计。当然如果是属于前者的需求,改成 tableViewFooter 也是可以的。

      本文标题:iOS 多级 UIScrollView 嵌套的实现方案

      本文链接:https://www.haomeiwen.com/subject/yksyrftx.html