前言
標(biāo)題中說(shuō)的可折疊TableView是指含有多個(gè)Section的TableView,可以按需求折疊或展開(kāi)指定的Section汞贸,實(shí)現(xiàn)的方式有很多種成洗,也比較簡(jiǎn)單。曾經(jīng)筆者也根據(jù)網(wǎng)上搜到的信息自己實(shí)現(xiàn)了一個(gè)可折疊的TableView彰导,但實(shí)現(xiàn)的方式略為笨拙蛔翅。最近項(xiàng)目沒(méi)那么緊,重新思考了下發(fā)現(xiàn)能有更優(yōu)雅的實(shí)現(xiàn)位谋,于是就有了這篇文章山析,也算是把這個(gè)過(guò)程記錄下來(lái),加深對(duì)其中運(yùn)用到的知識(shí)的印象掏父。下面筆者將會(huì)詳細(xì)介紹這種實(shí)現(xiàn)方式笋轨,只想看結(jié)果的讀者可以直接拖到最后查看效果圖和DEMO。
1损同、基本原理
可折疊TableView的實(shí)現(xiàn)方法很多翩腐,但是離不開(kāi)的一個(gè)關(guān)鍵點(diǎn)就是:** 保存TableView中每個(gè)Section的折疊狀態(tài),并根據(jù)這個(gè)狀態(tài)膏燃,判斷每個(gè)Section下該不該展示數(shù)據(jù)(Cell)茂卦。 **
根據(jù)這個(gè)關(guān)鍵點(diǎn),我們需要做的事情有兩個(gè)组哩。第一等龙,使用合適的方式保存每個(gè)Section的折疊狀態(tài)处渣。第二,根據(jù)狀態(tài)(折疊或展開(kāi))判斷是否展示數(shù)據(jù)蛛砰。
1.1 折疊狀態(tài)
既然確定了需要保存這個(gè)狀態(tài)罐栈,那我們需要考慮的就是這個(gè)狀態(tài)要怎么保存、保存在哪里泥畅。
年輕的我曾經(jīng)將這個(gè)折疊狀態(tài)保存在了接口返回的數(shù)據(jù)中荠诬,舉個(gè)例子,接口返回的是一個(gè)json列表位仁,列表中的每個(gè)對(duì)象代表一個(gè)Section柑贞,Cell的數(shù)據(jù)放在對(duì)象中的一個(gè)列表。
[
//section
{
name:"section1",
//row
list:[{},{}]
},...,{
}
]
這樣子處理雖然能保存狀態(tài)聂抢,但弊端就是每次從接口獲取到數(shù)據(jù)后都需要對(duì)數(shù)據(jù)源進(jìn)行處理钧嘶,在數(shù)據(jù)源中的每個(gè)section對(duì)象中加個(gè)state字段來(lái)標(biāo)識(shí)該section是否折疊。而且如果接口返回的是一個(gè)里面放著列表的json列表琳疏,就更麻煩了有决,還需要自己重新修改數(shù)據(jù)源的結(jié)構(gòu)。同時(shí)當(dāng)列表的折疊狀態(tài)發(fā)生改變空盼,還需要遍歷數(shù)據(jù)源數(shù)組书幕,來(lái)修改對(duì)應(yīng)的折疊狀態(tài)。
解決方案
所以在這次的實(shí)現(xiàn)中揽趾,筆者將這個(gè)狀態(tài)放到tableView本身身上來(lái)保存按咒,用一個(gè)屬性來(lái)保存它,以此將它跟數(shù)據(jù)源分開(kāi)來(lái)但骨,減低對(duì)數(shù)據(jù)源的污染。由于考慮到需要保存多個(gè)section的狀態(tài)智袭,而且每個(gè)section的狀態(tài)都是獨(dú)立的奔缠,只有展開(kāi)和折疊兩種,并且是無(wú)序的吼野,所以使用了NSSet保存校哎。section序號(hào)存在于這個(gè)Set中的section代表折疊,不存在代表展開(kāi)瞳步。
@property (strong, nonatomic) NSMutableSet *ww_foldState;
考慮到暴露狀態(tài)屬性給外界修改容易造成UI與狀態(tài)不一致闷哆,于是只把這個(gè)屬性放在了TableView的內(nèi)部維護(hù)使用。
1.2 判斷是否展現(xiàn)數(shù)據(jù)
年輕的我(再次出現(xiàn):D)在之前的實(shí)現(xiàn)方式是在UITableViewDataSource的代理方法中處理是否展現(xiàn)數(shù)據(jù)单起,如:
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSArray* dataList = self.dataHelper.dataList;
BOOL isFolded = [self.dataHelper isSectionFolded:section];
if(dataList != nil && section < dataList.count){
if(isFolded){
//折疊中返回0
return 0;
}else{
//需要展示的行數(shù)
return 1;
}
}
return 0;
}
向上面說(shuō)的抱怔,由于折疊狀態(tài)放到了數(shù)據(jù)源中保存,所以只好在代理方法中(代理也就是ViewController持有著數(shù)據(jù)源)根據(jù)數(shù)據(jù)源的狀態(tài)去判斷了嘀倒。這樣做對(duì)于新開(kāi)發(fā)的項(xiàng)目來(lái)說(shuō)也沒(méi)什么不太好的地方屈留,但假如是對(duì)舊項(xiàng)目進(jìn)行修改局冰,對(duì)需要改造的TableView的代理方法一個(gè)個(gè)去修改,就顯得有點(diǎn)麻煩了灌危。
解決方案
在這里我選擇了hook TableView中調(diào)用上面的代理方法的方法康二,并添加自己的邏輯。在上面返回行數(shù)的代理方法中打個(gè)斷點(diǎn)勇蝙,運(yùn)行一下沫勿,我們就可以從堆棧中看到是哪個(gè)方法在調(diào)用這個(gè)代理方法了。
然后根據(jù)TableView對(duì)應(yīng)section的折疊狀態(tài)味混,返回具體的行數(shù)产雹。
2、實(shí)現(xiàn)
下面將詳細(xì)介紹實(shí)現(xiàn)方式惜傲,涉及到ObjectC中的一些runtime知識(shí)洽故,不在這篇文章的討論范圍內(nèi),就不細(xì)說(shuō)了盗誊。
2.1 方法交換
由于需要hook TableView中的方法时甚,先給NSObject創(chuàng)建一個(gè)分類(lèi),用于添加method swizzle相關(guān)的方法哈踱。
#import <objc/runtime.h>
@interface NSObject (WWExtension)
+ (void)ww_swizzInstanceMethod:(SEL)methodOrig withMethod:(SEL)methodNew;
+ (void)ww_swizzClassMethod:(SEL)methodOrig withMethod:(SEL)methodNew;
@end
@implementation NSObject (WWExtension)
+ (void)ww_swizzInstanceMethod:(SEL)methodOrig withMethod:(SEL)methodNew
{
Method orig = class_getInstanceMethod(self, methodOrig);
Method new = class_getInstanceMethod(self, methodNew);
if(orig && new){
method_exchangeImplementations(orig, new);
}else{
NSLog(@"swizz method failed: %s", sel_getName(methodOrig));
}
}
+ (void)ww_swizzClassMethod:(SEL)methodOrig withMethod:(SEL)methodNew
{
Method orig = class_getClassMethod(self, methodOrig);
Method new = class_getClassMethod(self, methodNew);
if(orig && new){
method_exchangeImplementations(orig, new);
}else{
NSLog(@"swizz method failed: %s", sel_getName(methodOrig));
}
}
@end
記得導(dǎo)入<objc/runtime.h>
荒适。
2.2 TableView分類(lèi)
1.考慮到降低代碼侵入性,我選擇了用分類(lèi)的方式去實(shí)現(xiàn)這個(gè)功能开镣。給TableView創(chuàng)建一個(gè)分類(lèi)刀诬,用于添加折疊相關(guān)狀態(tài)與方法
@interface UITableView (WWFoldableTableView)
@property (assign, nonatomic) BOOL ww_foldable;
- (BOOL)ww_isSectionFolded:(NSInteger)section;
- (void)ww_foldSection:(NSInteger)section fold:(BOOL)fold;
@end
可以看到我在分類(lèi)中聲明了屬性,但編譯器并不會(huì)幫我們合成實(shí)例變量邪财,所以需要用runtime中的objc_getAssociatedObject
和objc_setAssociatedObject
方法在運(yùn)行時(shí)綁定對(duì)象陕壹。例如ww_foldable這個(gè)屬性:
static const char WWFoldableKey = '\0';
- (BOOL)ww_foldable
{
return [objc_getAssociatedObject(self, &WWFoldableKey) boolValue];
}
- (void)setWw_foldable:(BOOL)ww_foldable
{
[self willChangeValueForKey:@"ww_foldable"];
objc_setAssociatedObject(self, &WWFoldableKey, @(ww_foldable), OBJC_ASSOCIATION_ASSIGN);
[self didChangeValueForKey:@"ww_foldable"];
//initialize
if(ww_foldable && !self.ww_foldState){
NSMutableSet *foldState = [NSMutableSet set];
self.ww_foldState = foldState;
}
//clean up
if(!ww_foldable){
[self setWw_foldState:nil];
}
}
定義了的這兩個(gè)方法后當(dāng)我們?cè)诖a中調(diào)用tableView.ww_foldable時(shí),編譯器會(huì)根據(jù)需要將代碼自動(dòng)編譯成[tableView ww_foldable]或者[tableView setWw_foldable:]树埠。ww_foldState也是同樣的道理糠馆。
2.然后是兩個(gè)關(guān)鍵的用于交換的方法:
+ (void)load
{
[self ww_swizzInstanceMethod:@selector(_numberOfRowsInSection:) withMethod:@selector(ww__numberOfRowsInSection:)];
}
- (NSInteger)ww__numberOfRowsInSection:(NSInteger)section
{
if(!self.ww_foldState || !self.ww_foldState){
return [self ww__numberOfRowsInSection:section];
}
//根據(jù)折疊狀態(tài)返回行數(shù)
BOOL isFolded = [self ww_isSectionFolded:section];
return isFolded ? 0 : [self ww__numberOfRowsInSection:section];
}
在分類(lèi)中的+load
方法中將_numberOfRowsInSection:
私有實(shí)例方法與自己定義的方法交換。
在_numberOfRowsInSection:
中怎憋,TableView調(diào)用代理的相應(yīng)方法返回指定section中的行數(shù)又碌,我在原有的邏輯后面,添加根據(jù)折疊狀態(tài)返回行數(shù)的邏輯绊袋。
判斷指定section是否折疊毕匀,只需要判斷保存state的NSSet中是否存在該Section的索引即可。
3.調(diào)用
在上面兩步中癌别,已經(jīng)展示了這個(gè)組件的大部分代碼皂岔。其次調(diào)用這個(gè)組件的方式也很簡(jiǎn)單,引用這個(gè)組件的頭文件规个,然后正常定義我們的TableView凤薛,在最后加上tableView.ww_foldable = YES;
即可姓建,例如:
#pragma mark - getter
- (UITableView *)tableView
{
if(!_tableView){
_tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, ScreenWidth, ScreenHeight-StatusBarHeight-NavigationBarHeight) style:UITableViewStyleGrouped];
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
//設(shè)置可折疊
_tableView.ww_foldable = YES;
}
return _tableView;
}
然后在需要觸發(fā)折疊/展開(kāi)tableView的事件中調(diào)用:
[self.tableView ww_foldSection:section fold:![self.tableView ww_isSectionFolded:section]];
還有其他的代碼可以自行到Demo中查看~
運(yùn)行效果:
完整的Demo代碼在這里:
https://github.com/Tidusww/WWFoldableTableView
3、總結(jié)
通過(guò)思考需求的本質(zhì)原理缤苫,結(jié)合OC運(yùn)行時(shí)的特性速兔,讓我們以很小的代碼量(除去注釋150行代碼不到),很低的侵入性(僅引用一個(gè)頭文件活玲,無(wú)需繼承涣狗,正常定義tableView),十分方便的方式(1行代碼用于設(shè)置tableView)來(lái)優(yōu)雅地實(shí)現(xiàn)可折疊TableView舒憾。
另外也有一些容易踩到的坑在這里整理一下镀钓,如:
使用運(yùn)行時(shí)特性給tableview添加實(shí)例對(duì)象的時(shí)候要處理好關(guān)聯(lián)的類(lèi)型,不然容易出現(xiàn)超出預(yù)期的結(jié)果镀迂。
設(shè)置了某個(gè)section的折疊狀態(tài)后需要及時(shí)更新UI丁溅,讓UI跟狀態(tài)保持一致。
可以使用
[self reloadSections:[NSIndexSet indexSetWithIndex:section] withRowAnimation:UITableViewRowAnimationFade];
方法來(lái)動(dòng)態(tài)刷新某一section探遵,但要注意如果此時(shí)其他section的行數(shù)變化了(通過(guò)代理方法兩次獲取到的數(shù)目不同窟赏,在這里其實(shí)就是手動(dòng)修改了折疊狀態(tài),但沒(méi)有刷新tableView)會(huì)引起crash箱季。這種實(shí)現(xiàn)方式其實(shí)存在一個(gè)隱患涯穷,由于我們?cè)?code>+load方法中替換** 私有實(shí)例方法 **,假如蘋(píng)果對(duì)UITableView進(jìn)行優(yōu)化或者重構(gòu)(雖然可能性比較胁爻)拷况,導(dǎo)致邏輯變更、方法名有變等情況掘殴,就有可能影響到相關(guān)邏輯赚瘦,我們的方法也會(huì)不起作用了。所以需要在每個(gè)iOS新版本一直對(duì)其進(jìn)行維護(hù)奏寨,檢查方法是否改名或者邏輯是否改變蚤告。在舊版本的iOS系統(tǒng)中則只能在代理方法中根據(jù)當(dāng)前section的折疊狀態(tài)來(lái)返回元素個(gè)數(shù)了。
最后如果大家有更好的實(shí)現(xiàn)方式或者本文有什么紕漏服爷,歡迎在文章下面評(píng)論留言,一起討論获诈,一起進(jìn)步仍源。 _~