github傳送門:https://github.com/DawnWdf/DWCollectionView
支持Carthage安裝育特,請在Cartfile中填寫
github "DawnWdf/DWCollectionView"
支持cocoaPods安裝
pod "DWCollectionView"
為什么封裝CollectionView
項目中用到collectionView的地方很多,每一個VC里面都需要至少兩個代理方法,如果碰到頁面稍微復雜一點强胰,代理方法寫的更多毅该。當有N個頁面都需要寫相同的N個代理方法的時候须妻。菇篡。。波闹。酝豪。。
頁面需要多個collectionView精堕,需要在多個代理方法中判斷當前使用的collectionView是哪個
-
一個collectionView需要使用很多個不同樣子的cell孵淘。定義了N個cell,于是在每一個代理方法里面都有一個if-else來各種判斷歹篓。
當然瘫证,這個可以通過其他的方法來規(guī)避部分if-else判斷。比如說:當ModelA對應CellA,ModelB對應CellB……
- 讓所有的Model都遵循一個協(xié)議ModelPropocol庄撮。讓所有的cell都繼承自一個基類BaseCell背捌。
- ModelPropocol有一個方法cellNameFor,根據(jù)不用的ID來返回對應的cell的類名
- 在cellForItemAtIndexPath代理方法里面根據(jù)cell的類名創(chuàng)建cell洞斯,并執(zhí)行數(shù)據(jù)綁定的操作毡庆。
但是這個做法也有很多弊端,比如
- 在使用例如didSelectItemAtIndexPath方法時依然要if-else烙如,使用numberOfItemsInSection方法時也依然要判斷么抗。
- 同時也需要定義很多個id用來在相同model的情況下區(qū)分不同的cell。
- 業(yè)務要是再復雜一點亚铁,感覺就像是將每一個代理方法里面的if-else分發(fā)到了model中一樣蝇刀。model過于沉重,不僅保存了數(shù)據(jù)刀闷,還保存了對應的UI熊泵,還需要針對每一個代理方法做多余的操作仰迁。感覺已經(jīng)超出了重量級model該做的事情甸昏。
- 我們希望我們的model或者cell可以復用,當我們希望某一個model可以對接不同頁面不同cell的時候徐许。施蜜。。雌隅。翻默。缸沃。又或者希望我們的model和cell是可插拔式的。
有的項目中會有一些比較復雜和靈活的頁面修械。比如趾牧,整個頁面都是可以自由配置的。需要根據(jù)接口返回的數(shù)據(jù)來進行排版布局肯污。像是我現(xiàn)在做的項目翘单,除了要根據(jù)返回的數(shù)據(jù)來布局模塊的順序,還要求配置頁面上兩個cell之間是否有一個10像素的間距蹦渣,配置某一個cell上面或者下面是否有一個1像素的分割線哄芜。如果接口返回的數(shù)據(jù)結(jié)構(gòu)正好可以對接你的UI柬唯,那真是可喜可賀,如果無法對接失晴,需要自己判斷和組裝然后再渲染視圖。等你渲染了視圖拘央,接口要是升級或者字段調(diào)整师坎。胯陋。。袱箱。遏乔。发笔。萬一架構(gòu)的時候腦抽,或者寫代碼的時候犯二捻激,那真是“完美”前计。當然如果架構(gòu)夠好,這也是沒什么的丈屹。
我相信一定會有人做過類似的項目旺垒,踩過類似的坑的,對很多類似的骇钦、機械似的代碼表示厭煩竞漾。于是我封裝了collectionView畴蹭。當然我不會告訴你,同組的一個大神封裝了一個tableview讓我受益匪淺繁扎,燃起了自己也寫一個的欲望糊闽。這充分說明了右犹,跟著大神走,有肉吃盼忌。
封裝后可以渲染哪種頁面
-
普通列表
-
用戶中心
-
瀑布流(配合使用flowLayout)
還有多種多樣列表
只要配置好model與cell的對應關系,只要管理數(shù)據(jù)結(jié)構(gòu)就可以渲染視圖了跨嘉。
代碼如何實現(xiàn)
- 創(chuàng)建collection
就像創(chuàng)建一個普通UICollectionView一樣祠乃,除了把類名換成DWCollectionView以外兑燥,沒有其他操作。而且在不聲明UICollectionViewLayout的情況下寺庄,默認添加上去斗塘,免得崩潰亮靴。
DWCollectionView *cv= [[DWCollectionView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame)) collectionViewLayout:layout];
cv.backgroundColor = [UIColor whiteColor];
cv.delegate = self;
[self.view addSubview:cv];
- 配置model和cell的關系
- 創(chuàng)建collectionView的時候我并沒有配置dataSource = self;也沒有給collectionView注冊任何cell或者reuseview茧吊。
- 然而我們的collectionView需要配置數(shù)據(jù)源搓侄,并必須實現(xiàn)協(xié)議UICollectionViewDataSource中兩個方法。
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section;
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;
- 通常情況下cellForItemAtIndexPath的方法里面會寫model和cell的對應關系芯侥,通過數(shù)據(jù)源和IndexPath找到model柱查,再通過model去創(chuàng)建或者復用cell云石,然后給cell進行數(shù)據(jù)綁定汹忠。這個方法大概是所有代理方法中最重的一個。
- 我封裝后的collectionView則將注冊和model&cell之間的綁定簡化了一下奖地。
[self.collectionView registerViewAndModel:^(DWCollectionDelegateMaker *maker) {
maker.registerCell([TeamInfoCell class],[TeamInfo class])
.itemSize(^(NSIndexPath *indexPath, id data){
return CGSizeMake(100, 140);
})
.adapter(^(UICollectionViewCell *cell, NSIndexPath *indexPath, id data){
TeamInfoCell *newCell = (TeamInfoCell *)cell;
newCell.showImage = YES;
[newCell bindData:data];
})
.didSelect(^(NSIndexPath *indexPath, id data){
NSLog(@"did select block : 如果vc中實現(xiàn)了didSelect的代理方法参歹,則在此block后執(zhí)行");
});
}];
-
整體采用響應鏈式的編程方式犬庇。
registerViewAndModel方法承擔了cellForItemAtIndexPath全部的工作侨嘀。
maker.registerCell
的工作是告訴collectionView將model和cell綁定咬腕,只要數(shù)據(jù)源中出現(xiàn)model,就用對應的cell去渲染視圖纽帖。
maker. itemSize
替代了- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section;
maker. adapter
中的block則返回了每一個cell和當前cell對應的具體的數(shù)據(jù)懊直,在這里我們可以進行數(shù)據(jù)綁定,將model中具體的內(nèi)容渲染到cell中雕崩。這樣就節(jié)省了通過數(shù)據(jù)源和Indexpath來找到對應model再去渲染的麻煩盼铁。這里的cell我都遵循了
DWCollectionViewCellProtocol
協(xié)議尝偎,實現(xiàn)了- (void)bindData:(id)data;
方法冬念,以便在cell中做具體的綁定操作。maker.didSelect
的方法則完全是代理方法- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath;
的替代醒陆。
但是不是完全的替代刨摩,如果當前的VC中同時實現(xiàn)了這個代理方法世吨,那么block中的方法先執(zhí)行耘婚,然后執(zhí)行代理中的方法。
到這里嚷闭,一個簡單的collectionView已經(jīng)搭建完畢胞锰。這里通過幾個block完成了注冊視圖和至少三個必備代理方法兢榨。
我們再也不用滿VC去找每個代理方法然后做處理了。因為都在這了兼雄。
類似的案怯,header&footer的方法一致嘲碱。
//header
maker.registerHeader([UserCenterHeaderCollectionReusableView class],[UserCenterHeaderModel class])
.sizeConfiger(^(UICollectionViewLayout *layout,NSInteger section, id data){
return CGSizeMake(screenW, 33);
})
.adapter(^(UICollectionReusableView *reusableView,NSIndexPath *indexPath, id data) {
UserCenterHeaderCollectionReusableView *view = (UserCenterHeaderCollectionReusableView *)reusableView;
[view bindData:data];
});
//footer
maker.registerFooter([UICollectionReusableView class],[NSString class])
.sizeConfiger(^(UICollectionViewLayout *layout,NSInteger section, id data){
return CGSizeMake(screenW, 10);
})
.adapter(^(UICollectionReusableView *reusableView,NSIndexPath *indexPath, id data) {
reusableView.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.5];
});
-
給collectionView賦值麦锯。
由于封裝后的collectionView是數(shù)據(jù)驅(qū)動視圖的扶欣,給collectionView賦值就變得很重要千扶。所以數(shù)據(jù)也進行了一次封裝澎羞。
按照collectionView每一個section分為頭妆绞、尾和items將數(shù)據(jù)分為三個對應的部分@interface DWSection : NSObject <NSCoding> @property (nonatomic, strong) id headerData; @property (nonatomic, strong) id footerData; @property (nonatomic, strong) NSArray *items; @end
items里面存的就是所有需要展示的,并且已經(jīng)注冊過的model株茶。
[self.collectionView setData:data];
data為一個數(shù)組启盛,里面的每一個元素都是DWSection的對象技羔。
這樣做有一個好處就是堕阔,這個model很大的作用其實是脫離業(yè)務的超陆,針對視圖的model浦马。這樣如果接口數(shù)據(jù)結(jié)構(gòu)有變化晶默,而UI無變化航攒,只要將接口數(shù)據(jù)和model做對接就可以了漠畜。而且model可插拔憔狞,如果這個cell&model要移植到其他的項目或功能中,也只要在拼接數(shù)據(jù)的時候做點手腳就可以拍冠。舉個栗子:在做項目的時候庆杜,接口數(shù)據(jù)不能及時給出碟摆,就需要客戶端做一個假的數(shù)據(jù)焦履,我就根據(jù)接口文檔寫了dictionary來渲染cell嘉裤。但是當接入了接口,使用工程基本網(wǎng)絡框架后發(fā)現(xiàn)厢洞,它自動把返回的字典轉(zhuǎn)成了對應的業(yè)務相關model躺翻,里面一大堆跟UI無關的數(shù)據(jù)卫玖。于是我直接從業(yè)務model中抽出UI需要展示的屬性直接賦值給model&cell假瞬。
我在實際項目中使用的時候,這個model大多數(shù)都有幾個相同的屬性
@property (nonatomic, copy) NSString *title;//cell標題 @property (nonatomic, copy) NSString *imageUrl;//圖片 @property (nonatomic, copy) NSString *content;//內(nèi)容 @property (nonatomic, copy) NSString *scheme;//跳轉(zhuǎn)URL
主要說一下屬性scheme垄开。有一段時間router這個東西特別流行税肪,我想現(xiàn)在應該有很多項目也都有使用router益兄。而這個scheme就是為了router而存在的偏塞。我們的cell在點擊的時候大多要跳轉(zhuǎn)到一個二級頁面灸叼,有時需要傳遞一些參數(shù)庆捺,id/type什么的滔以。之前的做法則是在model中也聲明一個屬性ID,然后跳轉(zhuǎn)的時候傳值抵碟。
這里我們可以在viewModel中做數(shù)據(jù)轉(zhuǎn)換的時候拟逮,就根據(jù)要求將scheme拼接好敦迄,將需要傳遞的參數(shù)都放在sheme中凭迹。這樣點擊cell進行頁面跳轉(zhuǎn)的時候可以統(tǒng)一使用scheme進行頁面跳轉(zhuǎn)嗅绸,很大程度上降低了耦合度鱼鸠。如果所有的model的scheme屬性都一樣的話,就更加快捷漆弄,我們都不用關心我們拿到的id類型的數(shù)據(jù)data到底是哪個model了撼唾,只要它實現(xiàn)了scheme就行倒谷。像這樣if ([data respondsToSelector:NSSelectorFromString(@"scheme")]) { SEL sel = NSSelectorFromString(@"scheme"); IMP selImp = [data methodForSelector:sel]; id(*func)(id,SEL) = (void *)selImp; id scheme = func(data,sel); [ARouter jumpWithScheme:scheme title:nil other:nil]; }
到此渤愁,一個簡單的collectionView就全部完畢∨灯唬總結(jié)一下就三個步驟:
- 創(chuàng)建
- 綁定
- 添加數(shù)據(jù)源
其他的代理方法
我在封裝的時候收奔,希望對原有collectionView的侵入性最小坪哄。所以你會發(fā)現(xiàn)翩肌,只有上面提到的常用的代理方法是使用block的形式封裝在了一起禁悠。如果collectionView的功能比較多绷蹲,需要實現(xiàn)其他的代理方法祝钢,比如:- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath 拦英;
還是需要自己在vc中寫好的疤估。
所以霎冯,如果對layout有特殊的要求沈撞,依然可以實現(xiàn)相應的代理方法缠俺。如:
#pragma mark - UICollectionViewDelegateFlowLayout
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
return UIEdgeInsetsMake(10, 10, 10, 10);
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
return 10;
}
或者在創(chuàng)建的時候直接使用
UICollectionViewFlowLayout *sysLayout = [[UICollectionViewFlowLayout alloc] init];
sysLayout.estimatedItemSize = CGSizeMake(100, 150);
sysLayout.minimumLineSpacing = 50;
這些都是使用UICollectionView標準的方式壹士,不贅述躏救。
題外話:DWFlowLayout(自定義布局-瀑布流)盒使,不在封裝范圍內(nèi)忠怖。
缺憾
為了能更好的封裝代理方法抄瑟,我將代理做了重定向皮假。所以惹资,collectionView的代理在執(zhí)行的時候用的并不是聲明時指向的代理VC航闺,而是我自己的代理類DWCollectionDelegate潦刃。我只是在DWCollectionDelegate記錄了VC乖杠,并在對應的方法中調(diào)用了一次VC的方法胧洒。
然而collectionView遵循的協(xié)議有四個:
- UICollectionViewDataSource
- UICollectionViewDelegate
- UICollectionViewDelegateFlowLayout
- UIScrollViewDelegate
如果把所有的協(xié)議的代理方法都寫出來,那就是個天文數(shù)字菲饼。
所以我將不常用的代理方法使用runtime的方法做了轉(zhuǎn)換肾砂。
具體的代碼如下:
for (int i = 0; i < protocolMethodCount; i++) {
struct objc_method_description protocolObject = protocolDes[i];
SEL selector = protocolObject.name;
//originalDelegate是否實現(xiàn)此方法
BOOL isOriginalResponse = class_respondsToSelector(original , selector);
if (isOriginalResponse) {
Method originalMethod = class_getInstanceMethod(original, selector);
class_replaceMethod(aclass, selector, class_getMethodImplementation(original, selector), method_getTypeEncoding(originalMethod));
}
}
注釋中的originalDelegate代表的就是聲明時設置的代理VC『暝茫‘當前類’代表的就是DWCollectionDelegate通今。從代碼中可以看出我實際上是將兩個類的代理方法的imp互換了。所以就會出現(xiàn)一個問題肛根,例如代理方法-(void)scrollViewDidScroll:(UIScrollView *)scrollView
在當前類中不存在辫塌,但是在originalDelegate中存在了,替換了imp后派哲,在scrollViewDidScroll方法中的self就成了DWCollectionDelegate。所以這樣的代理方法中就要判斷一下self是哪個類芭届。同時储矩,由于DWCollectionDelegate已經(jīng)添加了這個代理方法,如果在其他的VC中不需要執(zhí)行這個代理方法褂乍,它會去實現(xiàn)過的方法中找持隧,所以也需要判斷delegate.originalDelegate是否為你需要的vc。遜斃了L悠B挪Α!low貨H焓怠呀狼!
UserCenterViewController.m中實現(xiàn)代理方法
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGPoint offset = scrollView.contentOffset;
if ([self isKindOfClass:[DWCollectionDelegate class]]) {
DWCollectionDelegate *delegate = (DWCollectionDelegate *)self;
id original = delegate.originalDelegate;
if ([original isKindOfClass:[UserCenterViewController class]]) {
UserCenterViewController *vc = (UserCenterViewController *)original;
[vc updateUserInforView:scrollView];
[vc updateNav:offset];
}
}else if([self isKindOfClass:[UserCenterViewController class]]){
[self updateUserInforView:scrollView];
[self updateNav:offset];
}
}
為了方便,我將這部分判斷做成了宏定義
#define DW_CheckSelfClass(calssName) \
calssName *trueSelf = self; \
if ([self isKindOfClass:[DWCollectionDelegate class]]) { \
DWCollectionDelegate *delegate = (DWCollectionDelegate *)self; \
id original = delegate.originalDelegate; \
if ([original isKindOfClass:[calssName class]]) { \
calssName *vc = (calssName *)original; \
trueSelf = vc;\
}else{ \
return; \
} \
}else if([self isKindOfClass:[calssName class]]){ \
trueSelf = self; \
} \
\
所以损离,新的代碼為
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGPoint offset = scrollView.contentOffset;
DW_CheckSelfClass(UserCenterViewController);
[trueSelf updateUserInforView:scrollView];
[trueSelf updateNav:offset];
}
這個問題暫時還沒有找到解決的方案哥艇。如果有大神出手,我會更新僻澎。如果路過的大神有方法貌踏,還請路見不平一聲吼。多謝窟勃!
在此列出已經(jīng)在DWCollectionDelegate實現(xiàn)的代理方法祖乳,在以下方法中可以不去判斷self的類型。
- UICollectionViewDelegateFlowLayout
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section
- UICollectionViewDelegate
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
- UICollectionViewDataSource
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView //不需要實現(xiàn)
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section //不需要實現(xiàn)
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath//不需要實現(xiàn)
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
封裝思路
- 代理方法重定向到DWCollectionViewDelegate拳恋,并保存原始代理類對象
- 使用字典保存所有信息凡资,包括注冊的cell和model的類名、block等
- 獲取所有代理方法,并將必要的代理方法做imp指向
- 在DWCollectionViewDelegate對應的代理方法中取得字典中保存的數(shù)據(jù)做block隙赁,或者調(diào)用原始類對象的代理
PS
暴露兩個NSObject的擴展類
- NSObject+Coding : 最快速度讓一個NSObject類支持coding
http://www.reibang.com/p/7e117a9fb2bd - NSObject+MulArgPerformSel : 讓某對象執(zhí)行特定多參數(shù)方法
http://www.reibang.com/p/f228a40e10a9