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

前面

目前項目功能作的差很少了. 須要完善和打磨, 今天須要爲全部的 TableView 列表頁沒有數據的時候展現一個友好的提示視圖, 一個一個改太麻煩了. 並且業務邏輯煩雜改起來也不容易. 因此花了點時間寫了一個小東西.在項目中按照項目的規範前綴使用了AN, 本身提取出來仍是按照本身的喜愛將前綴改成了XY.git

Demo

國際慣例, 先上 Demo github

Demo效果

優勢

  • 拖拽便可使用, 無需 import , 對原有代碼無需進行任何修改
  • 也能夠選擇實現方法, 實現快捷的自定義和徹底的自定義

感謝

今天搜索到了這篇文章, 也是個人思路來源. UITableView沒數據時用戶提示如何作編程

原理

無入侵

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

#import <objc/runtime.h>
複製代碼

目前我想到的思路是在 reloadData 的時候進行實現, 因此定義一個 xy_reloadData 方法, 而後和原有的 reloadData 方法進行交換.async

也就是說:優化

  • 在代碼中全部調用 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 方法的描述是ui

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 時;實現這個方法來加載後執行特定類的行爲。this

因此能夠實現無需 import 就能夠實現加載.spa

獲取 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 綁定一個屬性用來記錄是否已經加載完

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

/** 是否已經加載完成數據 */
- (BOOL)isInitFinish {
    id obj = objc_getAssociatedObject(self, _cmd);
    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 便可使用.

菜鳥一枚, 若是有大神不吝賜教, 必將感激涕零.

相關文章
相關標籤/搜索