YKPageControllerScrollView設(shè)計(jì)總結(jié)

概述

YKPageControllerScrollView 是一個 UIViewController 容器類的滾動視圖,支持 UIViewController 重用機(jī)制。YKPageControllerScrollView 類的設(shè)計(jì)參考了 UICollectionView 類,所以你會發(fā)現(xiàn),其接口以及代理方法和 UICollectionView 的是很相似的,使用上也是相似的。


如何與『容器內(nèi)容』交互?

容器有一個特性在我看來是很重要的,那就是『容器』和『容器內(nèi)容』之間的交互:『容器』告知『容器內(nèi)容』其狀態(tài)的變更。
對于YKPageControllerScrollView而言,這交互就是:告知『VC實(shí)例』的顯示狀態(tài)(將出現(xiàn) or 已出現(xiàn) or 已消失在視圖中)以及生命狀態(tài)(被容器回收了)的變更。

那么怎么達(dá)到以上的目的呢?此處是設(shè)計(jì)了一個協(xié)議 YKPageControllerScrollViewLifeCycleProtocol ,每個要放置入容器內(nèi)的 UIViewController 類都應(yīng)該去實(shí)現(xiàn)這么一個協(xié)議。協(xié)議內(nèi)容如下:

@protocol YKPageControllerScrollViewLifeCycleProtocol <NSObject>

@optional

- (void)controllerWillAppearInPageControllerScrollView;

- (void)controllerDidAppearInPageControllerScrollView;

- (void)controllerDidDisappearInPageControllerScrollView;

- (void)controllerDidBeReclaimedByPageControllerScrollView;

@end

實(shí)現(xiàn)了上述協(xié)議的 UIViewController 在狀態(tài)有變更時(shí),會得到來自容器的通知。


怎么通知VC實(shí)例顯示狀態(tài)的變更

YKPageControllerScrollView 繼承自 UIView,那在其內(nèi)部,到底是誰真正裝載了 UIViewController 的視圖內(nèi)容呢?

答案是:UICollectionView

YKPageControllerScrollView 是怎么獲取到VC實(shí)例的顯示狀態(tài)(將出現(xiàn) or 已出現(xiàn) or 已消失在視圖中)呢?正是借助了UICollectionViewUICollectionViewDelegate 里的相關(guān)回調(diào)方法:

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

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

但是上述的回調(diào)只是幫助 YKPageControllerScrollView 通知應(yīng)用層哪些VC實(shí)例 『將出現(xiàn)在視圖中』 和 『已消失在視圖中』 而已(包括通知對應(yīng)的VC實(shí)例),另外一個『已出現(xiàn)在視圖中』的狀態(tài),YKPageControllerScrollView 怎么通知應(yīng)用層呢?

方法其實(shí)也很簡單——當(dāng)YKPageControllerScrollView 里的視圖滑動
停止后,獲取當(dāng)前的VC實(shí)例,即可告知應(yīng)用層哪個VC實(shí)例已出現(xiàn)在視圖中(包括通知當(dāng)前VC實(shí)例)。

YKPageControllerScrollView 的滑動的產(chǎn)生,一個源自用戶手動滑動,一個源自程序接口 [UICollectionView setContentOffset:animated:]。

若是用戶手動滑動視圖,則在 - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView 回調(diào)中,執(zhí)行上述通知邏輯即可。

若是通過 [UICollectionView setContentOffset:animated:] 滑動視圖,則需要進(jìn)一步區(qū)分 animated 為 YES 和 NO 的情況:

  • 當(dāng)animated 為 YES 時(shí):

    此時(shí)在 - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView 回調(diào)中,執(zhí)行上述通知邏輯即可。

  • 當(dāng)animated 為 NO 時(shí):

    此時(shí)在 - (void)scrollViewDidScroll:(UIScrollView *)scrollView 回調(diào)中,判斷當(dāng)前的滑動是非用戶手動滑動且animated 為 NO的情況下,才執(zhí)行上述通知邏輯,具體代碼如下:

      - (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];
              //如果當(dāng)前VC還沒生成,則推遲發(fā)送通知
              if (currentVC) {
                  [self sendDidDisplayNotificationToViewController:currentVC];
                  
                  //停止?jié)L動后,回收可回收的VC
                  [self recycleViewController];
              }else{
                  [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(scrollViewDidEndScrollingAnimation:) object:scrollView];
                  [self performSelector:@selector(scrollViewDidEndScrollingAnimation:) withObject:scrollView afterDelay:1.0];
              }
          }
              
      }
    

UIViewController 重用機(jī)制

YKPageControllerScrollView 支持的 UIViewController 重用機(jī)制類似于 UICollectionViewcell 重用機(jī)制。

在應(yīng)用層面上,二者的使用是近似的:

  1. 通過 [YKPageControllerScrollView registerClassForController:class] 注冊可重用的 ViewController 類。
  2. 通過 [YKPageControllerScrollView dequeueReusableViewControllerWithReuseClass:class forIndex:index] 返回可重用的VC實(shí)例。若返回的實(shí)例為 nil,則由應(yīng)用層生成一個新的VC實(shí)例

那么,在 YKPageControllerScrollView 內(nèi),該機(jī)制是如何實(shí)現(xiàn)的呢?

重用機(jī)制,總體來說,涉及3個方面:

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

在解答上述3個問題前,先了解一下YKPageControllerScrollView 的輔助屬性:

@property (nonatomic,strong) NSMutableDictionary *dict4ReusableArray; 
@property (nonatomic,strong) NSMutableDictionary *dict4ActiveController; 
@property (nonatomic,strong) NSMutableArray *array4PendingControllerIndex; 
  • dict4ReusableArray 用于保存可重用的VC實(shí)例(沒加載到容器上的VC實(shí)例)數(shù)組
  • dict4ActiveController 用于保存正在使用的VC實(shí)例(加載到容器上的VC實(shí)例)
  • array4PendingControllerIndex 用于保存那些不可見的VC實(shí)例(加載到容器上,但是沒在可視區(qū)域的VC實(shí)例)的索引

VC是怎么得到的?

在回調(diào) - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 中,通知應(yīng)用層返回一個VC實(shí)例,并存放到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是怎么重新利用的?

在應(yīng)用層上,當(dāng) YKPageControllerScrollView 通知返回一個 VC實(shí)例時(shí),應(yīng)用層首先調(diào)用 YKPageControllerScrollViewdequeueReusableViewControllerWithReuseClass:forIndex: 方法獲取一個可重用的VC實(shí)例,若為nil,才生成一個VC實(shí)例給YKPageControllerScrollView。VC能夠重新利用的重點(diǎn)就在于 dequeueReusableViewControllerWithReuseClass:forIndex:的實(shí)現(xiàn):

- (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(執(zhí)行[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 根據(jù) class,從 dict4ReusableArray 字典中獲取對應(yīng)的VC重用數(shù)組,然后從數(shù)組中取出一個可用的VC實(shí)例。若數(shù)組為空,則返回 nil,由應(yīng)用層自己生成一個VC實(shí)例。

VC是怎么回收?

YKPageControllerScrollView 滑動過程中,會把顯示的VC實(shí)例的索引從 array4PendingControllerIndex 中移除,把消失的VC實(shí)例的索引添加到 array4PendingControllerIndex 中。
當(dāng) YKPageControllerScrollView 停止滑動后,執(zhí)行回收操作 [YKPageControllerScrollView recycleViewController]:把消失的且距離當(dāng)前索引的距離大于3的VC實(shí)例從 dict4ActiveController 字典中回收到對應(yīng)重用數(shù)組中?;厥詹僮骶唧w如下:

- (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 內(nèi)形成了VC實(shí)例的生成、回收、重用的閉環(huán)。


怎么通知VC實(shí)例生命狀態(tài)的變更

VC實(shí)例在YKPageControllerScrollView里的生命狀態(tài)的變更主要是:VC實(shí)例被 YKPageControllerScrollView 回收了。

所以,只需要在 YKPageControllerScrollView 執(zhí)行回收操作的時(shí)候,通知被回收的VC實(shí)例即可。具體代碼,可看[YKPageControllerScrollView recycleViewController] 。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,328評論 4 61
  • 有人說,喜歡回顧往事的人是因?yàn)椴粷M當(dāng)下現(xiàn)實(shí)。是啊,人們總覺得從前是最好的,從前的天很藍(lán),水很清,車馬郵件慢,一生只...
    他間閱讀 469評論 0 0
  • 默默堅(jiān)持打卡一段時(shí)間了,群里的小伙伴總是相互鼓勵,相互討論,我雖然在群內(nèi),但是只是來打個卡,簽個到就完了。 剛開始...
    鳳言飛語閱讀 342評論 0 4
  • 肥羊講故事真的很好聽,不像上課,就像故事會,一下子聽完了,總覺得意猶未盡。 愚蠢的人殺死下金蛋的鵝, 聰明的人為了...
    幸運(yùn)花開隨筆閱讀 402評論 0 4

友情鏈接更多精彩內(nèi)容