YKPageControllerScrollView设计总结

概述

YKPageControllerScrollView 是一个 UIViewController 容器类的滚动视图,支持 UIViewController 重用机制。YKPageControllerScrollView 类的设计参考了 UICollectionView 类,所以你会发现,其接口以及代理方法和 UICollectionView 的是很相似的,使用上也是相似的。


如何与『容器内容』交互?

容器有一个特性在我看来是很重要的,那就是『容器』和『容器内容』之间的交互:『容器』告知『容器内容』其状态的变更。
对于YKPageControllerScrollView而言,这交互就是:告知『VC实例』的显示状态(将出现 or 已出现 or 已消失在视图中)以及生命状态(被容器回收了)的变更。

那么怎么达到以上的目的呢?此处是设计了一个协议 YKPageControllerScrollViewLifeCycleProtocol ,每个要放置入容器内的 UIViewController 类都应该去实现这么一个协议。协议内容如下:

@protocol YKPageControllerScrollViewLifeCycleProtocol <NSObject>

@optional

- (void)controllerWillAppearInPageControllerScrollView;

- (void)controllerDidAppearInPageControllerScrollView;

- (void)controllerDidDisappearInPageControllerScrollView;

- (void)controllerDidBeReclaimedByPageControllerScrollView;

@end

实现了上述协议的 UIViewController 在状态有变更时,会得到来自容器的通知。


怎么通知VC实例显示状态的变更

YKPageControllerScrollView 继承自 UIView,那在其内部,到底是谁真正装载了 UIViewController 的视图内容呢?

答案是:UICollectionView

YKPageControllerScrollView 是怎么获取到VC实例的显示状态(将出现 or 已出现 or 已消失在视图中)呢?正是借助了UICollectionViewUICollectionViewDelegate 里的相关回调方法:

- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath;

- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath

但是上述的回调只是帮助 YKPageControllerScrollView 通知应用层哪些VC实例 『将出现在视图中』 和 『已消失在视图中』 而已(包括通知对应的VC实例),另外一个『已出现在视图中』的状态,YKPageControllerScrollView 怎么通知应用层呢?

方法其实也很简单——当YKPageControllerScrollView 里的视图滑动
停止后,获取当前的VC实例,即可告知应用层哪个VC实例已出现在视图中(包括通知当前VC实例)。

YKPageControllerScrollView 的滑动的产生,一个源自用户手动滑动,一个源自程序接口 [UICollectionView setContentOffset:animated:]

若是用户手动滑动视图,则在 - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView 回调中,执行上述通知逻辑即可。

若是通过 [UICollectionView setContentOffset:animated:] 滑动视图,则需要进一步区分 animated 为 YES 和 NO 的情况:

  • animated 为 YES 时:

    此时在 - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView 回调中,执行上述通知逻辑即可。

  • animated 为 NO 时:

    此时在 - (void)scrollViewDidScroll:(UIScrollView *)scrollView 回调中,判断当前的滑动是非用户手动滑动且animated 为 NO的情况下,才执行上述通知逻辑,具体代码如下:

      - (void)scrollViewDidScroll:(UIScrollView *)scrollView
      {
          if (!self.isScrollWithAnim && !scrollView.isTracking && !scrollView.isDragging && !scrollView.isDecelerating) {
              self.currentIndex = (NSInteger)(scrollView.contentOffset.x / self.frame.size.width);
              
              UIViewController<YKPageControllerScrollViewLifeCycleProtocol> *currentVC = [self currentViewController];
              //如果当前VC还没生成,则推迟发送通知
              if (currentVC) {
                  [self sendDidDisplayNotificationToViewController:currentVC];
                  
                  //停止滚动后,回收可回收的VC
                  [self recycleViewController];
              }else{
                  [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(scrollViewDidEndScrollingAnimation:) object:scrollView];
                  [self performSelector:@selector(scrollViewDidEndScrollingAnimation:) withObject:scrollView afterDelay:1.0];
              }
          }
              
      }
    

UIViewController 重用机制

YKPageControllerScrollView 支持的 UIViewController 重用机制类似于 UICollectionViewcell 重用机制。

在应用层面上,二者的使用是近似的:

  1. 通过 [YKPageControllerScrollView registerClassForController:class] 注册可重用的 ViewController 类。
  2. 通过 [YKPageControllerScrollView dequeueReusableViewControllerWithReuseClass:class forIndex:index] 返回可重用的VC实例。若返回的实例为 nil,则由应用层生成一个新的VC实例

那么,在 YKPageControllerScrollView 内,该机制是如何实现的呢?

重用机制,总体来说,涉及3个方面:

  • VC是怎么得到的?
  • VC是怎么重新利用的?
  • VC是怎么回收的?

在解答上述3个问题前,先了解一下YKPageControllerScrollView 的辅助属性:

@property (nonatomic,strong) NSMutableDictionary *dict4ReusableArray; 
@property (nonatomic,strong) NSMutableDictionary *dict4ActiveController; 
@property (nonatomic,strong) NSMutableArray *array4PendingControllerIndex; 
  • dict4ReusableArray 用于保存可重用的VC实例(没加载到容器上的VC实例)数组
  • dict4ActiveController 用于保存正在使用的VC实例(加载到容器上的VC实例)
  • array4PendingControllerIndex 用于保存那些不可见的VC实例(加载到容器上,但是没在可视区域的VC实例)的索引

VC是怎么得到的?

在回调 - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 中,通知应用层返回一个VC实例,并存放到dict4ActiveController 字典中:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:YKPageControllerScrollViewCellIdentifier forIndexPath:indexPath];
    
    NSInteger index = indexPath.row;
    self.currentIndex = index;
    
    if (self.delegate && [self.delegate respondsToSelector:@selector(pageControllerScrollView:controllerForItemAtIndex:)]) {
        UIViewController<YKPageControllerScrollViewLifeCycleProtocol> *vc = [self.delegate pageControllerScrollView:self controllerForItemAtIndex:index];
        
        vc.view.frame = cell.contentView.bounds;
        [cell.contentView addSubview:vc.view];
        [self.containerViewController addChildViewController:vc];
        [self.dict4ActiveController setObject:vc forKey:@(index)];
        [self.array4PendingControllerIndex removeObject:@(index)];
    }
    
    //NSLog(@"cellForItemAtIndex:%d",index);
    
    return cell;
}

VC是怎么重新利用的?

在应用层上,当 YKPageControllerScrollView 通知返回一个 VC实例时,应用层首先调用 YKPageControllerScrollViewdequeueReusableViewControllerWithReuseClass:forIndex: 方法获取一个可重用的VC实例,若为nil,才生成一个VC实例给YKPageControllerScrollView。VC能够重新利用的重点就在于 dequeueReusableViewControllerWithReuseClass:forIndex:的实现:

- (nullable UIViewController<YKPageControllerScrollViewLifeCycleProtocol> *)dequeueReusableViewControllerWithReuseClass:(nonnull Class)reuseClass forIndex:(NSInteger)index
{
    UIViewController<YKPageControllerScrollViewLifeCycleProtocol> *reusableVC = nil;
    
    NSString *identifier = [reuseClass description];
    NSMutableArray *reusableArray = [self.dict4ReusableArray objectForKey:identifier];
    
    //若reusableArray为nil,说明没有注册reuseClass(执行[YKPageControllerScrollView registerClassForController:reuseClass])
    if (reuseClass && reusableArray) {
        UIViewController<YKPageControllerScrollViewLifeCycleProtocol> *vc = [self.dict4ActiveController objectForKey:@(index)];
        if (vc) {
            reusableVC = vc;
        }else{
            vc = [reusableArray firstObject];
            if (vc) {
                reusableVC = vc;
                [reusableArray removeObject:vc];
            }
        }
    }
    
    return reusableVC;
}

YKPageControllerScrollView 根据 class,从 dict4ReusableArray 字典中获取对应的VC重用数组,然后从数组中取出一个可用的VC实例。若数组为空,则返回 nil,由应用层自己生成一个VC实例。

VC是怎么回收?

YKPageControllerScrollView 滑动过程中,会把显示的VC实例的索引从 array4PendingControllerIndex 中移除,把消失的VC实例的索引添加到 array4PendingControllerIndex 中。
YKPageControllerScrollView 停止滑动后,执行回收操作 [YKPageControllerScrollView recycleViewController]:把消失的且距离当前索引的距离大于3的VC实例从 dict4ActiveController 字典中回收到对应重用数组中。回收操作具体如下:

- (void)recycleViewController
{
   NSArray *tempArray = [NSArray arrayWithArray:self.array4PendingControllerIndex];
   for (NSNumber *indexNum in tempArray) {
       NSInteger index = [indexNum integerValue];
       
       if (labs(self.currentIndex - index) >= 3 ) {
           UIViewController<YKPageControllerScrollViewLifeCycleProtocol> *vc = [self.dict4ActiveController objectForKey:@(index)];
           
           if (vc) {
               [vc.view removeFromSuperview];
               [vc removeFromParentViewController];
               
               if ([vc respondsToSelector:@selector(controllerDidBeReclaimedByPageControllerScrollView)]) {
                   [vc controllerDidBeReclaimedByPageControllerScrollView];
               }
               
               [self.dict4ActiveController removeObjectForKey:@(index)];
               [self.array4PendingControllerIndex removeObject:@(index)];
               
               Class reuseClass = [vc class];
               NSString *identifier = [reuseClass description];
               NSMutableArray *reusableArray = [self.dict4ReusableArray objectForKey:identifier];
               [reusableArray addObject:vc];
           }
       }
   }
}

至此,YKPageControllerScrollView 内形成了VC实例的生成、回收、重用的闭环。


怎么通知VC实例生命状态的变更

VC实例在YKPageControllerScrollView里的生命状态的变更主要是:VC实例被 YKPageControllerScrollView 回收了。

所以,只需要在 YKPageControllerScrollView 执行回收操作的时候,通知被回收的VC实例即可。具体代码,可看[YKPageControllerScrollView recycleViewController]

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,609评论 4 59
  • 有人说,喜欢回顾往事的人是因为不满当下现实。是啊,人们总觉得从前是最好的,从前的天很蓝,水很清,车马邮件慢,一生只...
    他间阅读 407评论 0 0
  • 默默坚持打卡一段时间了,群里的小伙伴总是相互鼓励,相互讨论,我虽然在群内,但是只是来打个卡,签个到就完了。 刚开始...
    凤言飞语阅读 227评论 0 4
  • 肥羊讲故事真的很好听,不像上课,就像故事会,一下子听完了,总觉得意犹未尽。 愚蠢的人杀死下金蛋的鹅, 聪明的人为了...
    幸运花开随笔阅读 272评论 0 4