iOS 0行代碼實現 TableView 無數據時展示占位視圖

前面

目前項目功能做的差不多了. 需要完善和打磨, 今天需要為所有的 TableView 列表頁沒有數據的時候展示一個友好的提示視圖, 一個一個改太麻煩了. 而且業務邏輯煩雜改起來也不容易. 所以花了點時間寫了一個小東西.在項目中按照項目的規范前綴使用了AN, 自己提取出來還是按照自己的喜好將前綴改為了XY.

Demo

國際慣例, 先上 Demo

Demo效果

優點

  • 拖拽即可使用, 無需 import , 對原有代碼無需進行任何修改
  • 也可以選擇實現方法, 實現快捷的自定義和完全的自定義

感謝

今天搜索到了這篇文章, 也是我的思路來源.
UITableView沒數據時用戶提示如何做

原理

無入侵

使用 Runtime 交換方法實現對原有代碼無入侵.
創建一個 TableView 的分類, 在 .m 中

#import <objc/runtime.h>

目前我想到的思路是在 reloadData 的時候進行實現, 所以定義一個
xy_reloadData 方法, 然后和原有的 reloadData 方法進行交換.

也就是說:

  • 在代碼中所有調用 reloadData 的方法最終會調用我們自定義的 xy_reloadData 方法.
  • 我們 xy_reloadData 方法中, 如果想調用系統的 reloadData 方法, 則需要調用 xy_reloadData 方法.
+ (void)load {
    
    Method reloadData    = class_getInstanceMethod(self, @selector(reloadData));
    Method xy_reloadData = class_getInstanceMethod(self, @selector(xy_reloadData));
    method_exchangeImplementations(reloadData, xy_reloadData);
}

對 load 方法的描述是

Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading.
當一個 分類 添加到 Objective-C Runtime 時;實現這個方法來加載后執行特定類的行為。

所以可以實現無需 import 就可以實現加載.

獲取 TableView 的數據量

TableView 有可能有多個 Sections 每個 Section 都有可能有很多 Cell. 所以不能單單判斷第一個 Section 是否有數據. 所以要:

  • 獲取 Section 的數量
  • 獲取每一個 Section 當中 Cell 的數量
    NSInteger numberOfSections = [self numberOfSections];
    BOOL havingData = NO;
    for (NSInteger i = 0; i < numberOfSections; i++) {
        if ([self numberOfRowsInSection:i] > 0) {
            havingData = YES;
            break;
        }
    }

這樣這個布爾值 havingData 即是是否有數據的標記.

如何實現 reloadData 完成之后再獲取數量.

因為 TableView 的 reloadData 方法具體實現是異步的.想要獲取到加載完成的狀態有兩種方法

  1. 使用 layoutIfNeeded 方法
  2. 獲取主隊列異步執行

第一種方法實現代碼為:

    [self xy_reloadData];
    [self layoutIfNeeded];
    //接下來的代碼

這樣的話線程會一直阻塞, 當然我們不希望原來業務代碼中的 reloadData 會阻塞, 直到加載完成之后再繼續執行代碼.

所以我選擇第二種方法

    [self xy_reloadData];
    dispatch_async(dispatch_get_main_queue(), ^{
        //接下來的代碼
    });

那么我們 xy_reloadData 中的方法實現為:

- (void)xy_reloadData {
    
    [self xy_reloadData];
    
    //  刷新完成之后檢測數據量
    dispatch_async(dispatch_get_main_queue(), ^{
        
        NSInteger numberOfSections = [self numberOfSections];
        BOOL havingData = NO;
        for (NSInteger i = 0; i < numberOfSections; i++) {
            if ([self numberOfRowsInSection:i] > 0) {
                havingData = YES;
                break;
            }
        }
        
        [self xy_havingData:havingData];
    });
}

展示一個占位視圖

TableView 有一個 backgroundView 的屬性可以很好的勝任這個需求
可以根據 havingData 的狀態來進行賦值

- (void)xy_havingData:(BOOL)havingData {
    if (havingData) {
        self.backgroundView = nil;
    } else {
        self.backgroundView = 自定義視圖;
    }
}

如何讓控制器自定義視圖

當然我們不滿足于簡簡單單的視圖的需求, 我們希望對應的控制器可以根據自己的需求自定義自己的視圖.

我們最習慣的方法當然是在 TableView 的代理類(通常是控制器)中去處理 TableView 的一些邏輯

那么假設我們希望代理類實現一個方法 xy_noDataView

    if ([self.delegate respondsToSelector:@selector(xy_noDataView)]) {
        self.backgroundView = [self.delegate performSelector:@selector(xy_noDataView)];
        return ;
    }

這個地方會有一個編譯警告, 我選擇在 .m 文件中定義一個 protocol 來消除, 我還定義了一些其他的方法來更好的完成我的需求.

/**
 消除警告
 */
@protocol XYTableViewDelegate <NSObject>
@optional
- (UIView   *)xy_noDataView;                //  完全自定義占位圖
- (UIImage  *)xy_noDataViewImage;           //  使用默認占位圖, 提供一張圖片,    可不提供, 默認不顯示
- (NSString *)xy_noDataViewMessage;         //  使用默認占位圖, 提供顯示文字,    可不提供, 默認為暫無數據
- (UIColor  *)xy_noDataViewMessageColor;    //  使用默認占位圖, 提供顯示文字顏色, 可不提供, 默認為灰色
- (NSNumber *)xy_noDataViewCenterYOffset;   //  使用默認占位圖, CenterY 向下的偏移量
@end

之所以沒有在. h 中聲明, 然后要求控制器實現我們的代理, 然后在去實現方法是想盡可能的無侵入, 契約式編程, 按規則實現方法既可以生效.

我希望能實現 拖來即用, 想扔就扔

我還實現了一些簡單的功能. 詳細的可以查看 Demo.

完整的xy_havingData方法如下:

- (void)xy_havingData:(BOOL)havingData {
    
    //  不需要顯示占位圖
    if (havingData) {
        self.backgroundView = nil;
        return ;
    }
    
    //  不需要重復創建
    if (self.backgroundView) {
        return ;
    }
    
    //  自定義了占位圖
    if ([self.delegate respondsToSelector:@selector(xy_noDataView)]) {
        self.backgroundView = [self.delegate performSelector:@selector(xy_noDataView)];
        return ;
    }
    
    //  使用自帶的
    UIImage  *img   = nil;
    NSString *msg   = @"暫無數據";
    UIColor  *color = [UIColor lightGrayColor];
    CGFloat  offset = 0;
    
    //  獲取圖片
    if ([self.delegate    respondsToSelector:@selector(xy_noDataViewImage)]) {
        img = [self.delegate performSelector:@selector(xy_noDataViewImage)];
    }
    //  獲取文字
    if ([self.delegate    respondsToSelector:@selector(xy_noDataViewMessage)]) {
        msg = [self.delegate performSelector:@selector(xy_noDataViewMessage)];
    }
    //  獲取顏色
    if ([self.delegate      respondsToSelector:@selector(xy_noDataViewMessageColor)]) {
        color = [self.delegate performSelector:@selector(xy_noDataViewMessageColor)];
    }
    //  獲取偏移量
    if ([self.delegate        respondsToSelector:@selector(xy_noDataViewCenterYOffset)]) {
        offset = [[self.delegate performSelector:@selector(xy_noDataViewCenterYOffset)] floatValue];
    }
    
    //  創建占位圖
    self.backgroundView = [self xy_defaultNoDataViewWithImage  :img message:msg color:color offsetY:offset];
}

實現了, 可以通過完全自定義 View 的方法實現完全自定義, 也可以使用自帶的一些樣式, 指定圖片, 文字, 文字顏色, 以及位置偏移量, 當然其中任何一個都是可以不指定的, 使用默認設定.

界面的一些代碼


/**
 默認的占位圖
 */
- (UIView *)xy_defaultNoDataViewWithImage:(UIImage *)image message:(NSString *)message color:(UIColor *)color offsetY:(CGFloat)offset {
    
    //  計算位置, 垂直居中, 圖片默認中心偏上.
    CGFloat sW = self.bounds.size.width;
    CGFloat cX = sW / 2;
    CGFloat cY = self.bounds.size.height * (1 - 0.618) + offset;
    CGFloat iW = image.size.width;
    CGFloat iH = image.size.height;
    
    //  圖片
    UIImageView *imgView = [[UIImageView alloc] init];
    imgView.frame        = CGRectMake(cX - iW / 2, cY - iH / 2, iW, iH);
    imgView.image        = image;
    
    //  文字
    UILabel *label       = [[UILabel alloc] init];
    label.font           = [UIFont systemFontOfSize:17];
    label.textColor      = color;
    label.text           = message;
    label.textAlignment  = NSTextAlignmentCenter;
    label.frame          = CGRectMake(0, CGRectGetMaxY(imgView.frame) + 24, sW, label.font.lineHeight);
    
    //  視圖
    XYNoDataView *view   = [[XYNoDataView alloc] init];
    [view addSubview:imgView];
    [view addSubview:label];
    
    //  實現跟隨 TableView 滾動
    [view addObserver:self forKeyPath:kXYNoDataViewObserveKeyPath options:NSKeyValueObservingOptionNew context:nil];
    return view;
}

細節優化

如何實現頁面加載的時候不展示占位圖

在 TableView 顯示到界面上時, 相當于調用了 reloadData 方法, 所以按照我們目前的邏輯會先展示一個占位圖, 然后數據加載完成后, 再次調用 reloadData 方法以隱藏占位圖.

數據加載之前, 我們肯定不希望展示無數據的占位圖, 所以可以忽略掉第一次調用 reloadData 的處理, 在 xy_reloadData 方法中增加如下校驗在 [self xy_reloadData]; 之后, 如果沒有加載完成數據時, 我們默認當做有數據去處理, 即相當于占位圖不顯示. 然后記錄一下, 數據已經加載完成了

//  忽略第一次加載
    if (![self isInitFinish]) {
        [self xy_havingData:YES];
        [self setIsInitFinish:YES];
        return ;
    }

為 TableView 綁定一個屬性用來記錄是否已經加載完

/// 加載完數據的標記屬性名
static NSString * const kXYTableViewPropertyInitFinish = @"kXYTableViewPropertyInitFinish";

/**
 設置已經加載完成數據了
 */
- (void)setIsInitFinish:(BOOL)finish {
    objc_setAssociatedObject(self, &kXYTableViewPropertyInitFinish, @(finish), OBJC_ASSOCIATION_ASSIGN);
}

/**
 是否已經加載完成數據
 */
- (BOOL)isInitFinish {
    id obj = objc_getAssociatedObject(self, &kXYTableViewPropertyInitFinish);
    return [obj boolValue];
}

滾動時如何讓占位圖跟隨 TableView 的滾動而滾動.

因為我們的占位圖是賦值在 TableView 的 backgroundView 屬性上的, 相當于增加到了 TableView 上, 通過調試可以發現, 在 TableView 滾動 contentOffset 改變時, backgroundViewframe.origin.y也是同步改變的, 所以我們看起來無論 TableView 怎么滾動占位圖都是無動于衷的, 如果我們想讓占位圖跟隨滾動的話, 只要取消掉backgroundViewframe.origin.y 的同步更新就好了, 也就是說要保證 frame.origin.y 的值一直為0.

我這里沒有找到更好的辦法, 暫時使用 KVO 來實現, 記得 View 銷毀的時候要移除 KVO 的監聽, 詳細實現可以看 Demo 啦...

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:kXYNoDataViewObserveKeyPath]) {
        
        /**
         在 TableView 滾動 ContentOffset 改變時, 會同步改變 backgroundView 的 frame.origin.y
         可以實現, backgroundView 位置相對于 TableView 不動, 但是我們希望
         backgroundView 跟隨 TableView 的滾動而滾動, 只能強制設置 frame.origin.y 永遠為 0
         兼容 MJRefresh
         */
        CGRect frame = [[change objectForKey:NSKeyValueChangeNewKey] CGRectValue];
        if (frame.origin.y != 0) {
            frame.origin.y  = 0;
            self.backgroundView.frame = frame;
        }
    }
}

如果不想顯示占位圖怎么辦?

在對應的控制器實現如下方法即可

- (NSString *)xy_noDataViewMessage {
    return @"";
}

關于分割線

在我上面提到的那篇文章中. 在修改 backgroundView 屬性的同時修改了 TableView 的 separatorStyle 屬性, 沒數據的時候將分割線取消掉, 有數據的時候在添加上, 可是我在項目中使用的 TableView 的分割線 separatorStyle 風格不一. 所以我沒有修改分割線屬性, 如果想讓 TableView 沒有數據的時候隱藏分割線, 可以看我的 Demo 在對應的控制器添加這樣一行代碼即可.

    self.tableView.tableFooterView = [UIView new];

最后

CollectionView 同理, 代碼復制一遍, 將獲取數據量的地方, 獲取每個 Section 中 Cell 的數量的 numberOfRowsInSection 方法改為 numberOfItemsInSection 即可使用.

菜鳥一枚, 如果有大神不吝賜教, 必將感激不盡.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,732評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,214評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,781評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,315評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,699評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,882評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,441評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,189評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,388評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,613評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,112評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,334評論 2 377

推薦閱讀更多精彩內容