iOS10 中優(yōu)雅地實(shí)現(xiàn)可折疊UITableView的思路

前言

標(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è)代理方法了。


-[UITableView _numberOfRowsInSection:]就是我們要hook的方法

然后根據(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_getAssociatedObjectobjc_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效果

完整的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舒憾。

另外也有一些容易踩到的坑在這里整理一下镀钓,如:

  1. 使用運(yùn)行時(shí)特性給tableview添加實(shí)例對(duì)象的時(shí)候要處理好關(guān)聯(lián)的類(lèi)型,不然容易出現(xiàn)超出預(yù)期的結(jié)果镀迂。

  2. 設(shè)置了某個(gè)section的折疊狀態(tài)后需要及時(shí)更新UI丁溅,讓UI跟狀態(tài)保持一致。

  3. 可以使用[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箱季。

  4. 這種實(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)步仍源。 _~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市舔涎,隨后出現(xiàn)的幾起案子笼踩,更是在濱河造成了極大的恐慌,老刑警劉巖亡嫌,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嚎于,死亡現(xiàn)場(chǎng)離奇詭異掘而,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)于购,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)袍睡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人肋僧,你說(shuō)我怎么就攤上這事斑胜。” “怎么了嫌吠?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵止潘,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我辫诅,道長(zhǎng)凭戴,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任炕矮,我火速辦了婚禮么夫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吧享。我一直安慰自己魏割,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布钢颂。 她就那樣靜靜地躺著钞它,像睡著了一般。 火紅的嫁衣襯著肌膚如雪殊鞭。 梳的紋絲不亂的頭發(fā)上遭垛,一...
    開(kāi)封第一講書(shū)人閱讀 52,475評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音操灿,去河邊找鬼锯仪。 笑死,一個(gè)胖子當(dāng)著我的面吹牛趾盐,可吹牛的內(nèi)容都是我干的庶喜。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼救鲤,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼久窟!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起本缠,我...
    開(kāi)封第一講書(shū)人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤斥扛,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后丹锹,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體稀颁,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡芬失,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了匾灶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棱烂。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖粘昨,靈堂內(nèi)的尸體忽然破棺而出垢啼,到底是詐尸還是另有隱情,我是刑警寧澤张肾,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布芭析,位于F島的核電站,受9級(jí)特大地震影響吞瞪,放射性物質(zhì)發(fā)生泄漏馁启。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一芍秆、第九天 我趴在偏房一處隱蔽的房頂上張望惯疙。 院中可真熱鬧,春花似錦妖啥、人聲如沸霉颠。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蒿偎。三九已至,卻和暖如春怀读,著一層夾襖步出監(jiān)牢的瞬間诉位,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工菜枷, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留苍糠,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓啤誊,卻偏偏與公主長(zhǎng)得像岳瞭,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蚊锹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容