概述
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 已消失在視圖中)呢?正是借助了UICollectionView 的 UICollectionViewDelegate 里的相關(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ī)制類似于 UICollectionView 的 cell 重用機(jī)制。
在應(yīng)用層面上,二者的使用是近似的:
- 通過
[YKPageControllerScrollView registerClassForController:class]注冊可重用的ViewController類。 - 通過
[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)用 YKPageControllerScrollView 的dequeueReusableViewControllerWithReuseClass: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] 。