TableView 无数据时展示占位视图

UITableView+NoDataView.m

#import "UITableView+NoDataView.h"
#import "NoDataView.h"
#import <objc/runtime.h>

@protocol TableViewDelegate <NSObject>
@optional
- (UIView *)noDataView;
- (UIImage *)noDataViewImage;
- (NSString *)noDataViewMessage;
- (UIColor *)noDataViewMessageColor;
- (NSNumber *)noDataViewCenterYOffset;

@end

@implementation UITableView (NoDataView)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method reloadData = class_getInstanceMethod(self, @selector(reloadData));
        Method replace_reloadData = class_getInstanceMethod(self, @selector(replace_reloadData));
        method_exchangeImplementations(reloadData, replace_reloadData);
        
        Method dealloc = class_getInstanceMethod(self, NSSelectorFromString(@"dealloc"));
        Method replace_dealloc = class_getInstanceMethod(self, @selector(replace_dealloc));
        method_exchangeImplementations(dealloc, replace_dealloc);
    });
}

- (void)replace_reloadData {
    [self replace_reloadData];
    
    //  忽略第一次加载
    if (![self isInitFinish]) {
        [self havingData:YES];
        [self setIsInitFinish:YES];
        return ;
    }
    
    //  刷新完成之后检测数据量
    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 havingData:havingData];
    });
}


/**
 展示占位图
 */
- (void)havingData:(BOOL)havingData {
    
    //  不需要显示占位图
    if (havingData) {
        [self freeNoDataViewIfNeeded];
        self.backgroundView = nil;
        return ;
    }
    
    //  不需要重复创建
    if (self.backgroundView) {
        return ;
    }
    
    //  自定义了占位图
    if ([self.delegate respondsToSelector:@selector(noDataView)]) {
        self.backgroundView = [self.delegate performSelector:@selector(noDataView)];
        return ;
    }
    
    //  使用自带的
    UIImage  * img   = nil;
    NSString * msg   = @"暂无数据";
    UIColor  * color = [UIColor lightGrayColor];
    CGFloat  offset  = 0;
    
    //  获取图片
    if ([self.delegate    respondsToSelector:@selector(noDataViewImage)]) {
        img = [self.delegate performSelector:@selector(noDataViewImage)];
    }
    //  获取文字
    if ([self.delegate    respondsToSelector:@selector(noDataViewMessage)]) {
        msg = [self.delegate performSelector:@selector(noDataViewMessage)];
    }
    //  获取颜色
    if ([self.delegate      respondsToSelector:@selector(noDataViewMessageColor)]) {
        color = [self.delegate performSelector:@selector(noDataViewMessageColor)];
    }
    //  获取偏移量
    if ([self.delegate        respondsToSelector:@selector(noDataViewCenterYOffset)]) {
        offset = [[self.delegate performSelector:@selector(noDataViewCenterYOffset)] floatValue];
    }
    
    //  创建占位图
    self.backgroundView = [self defaultNoDataViewWithImage  :img message:msg color:color offsetY:offset];
}

/**
 默认的占位图
 */
- (UIView *)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);
    
    //  视图
    NoDataView *view   = [[NoDataView alloc] init];
    [view addSubview:imgView];
    [view addSubview:label];
    
    //  实现跟随 TableView 滚动
    [view addObserver:self forKeyPath:kNoDataViewObserveKeyPath options:NSKeyValueObservingOptionNew context:nil];
    return view;
}


/**
 监听
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:kNoDataViewObserveKeyPath]) {
        
        /**
         在 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;
        }
    }
}



#pragma mark - 属性

// 加载完数据的标记属性名
static NSString * const kTableViewPropertyInitFinish = @"kTableViewPropertyInitFinish";

/**
 设置已经加载完成数据了
 */
- (void)setIsInitFinish:(BOOL)finish {
    objc_setAssociatedObject(self, &kTableViewPropertyInitFinish, @(finish), OBJC_ASSOCIATION_ASSIGN);
}

/**
 是否已经加载完成数据
 */
- (BOOL)isInitFinish {
    id obj = objc_getAssociatedObject(self, &kTableViewPropertyInitFinish);
    return [obj boolValue];
}

/**
 移除 KVO 监听
 */
- (void)freeNoDataViewIfNeeded {
    
    if ([self.backgroundView isKindOfClass:[NoDataView class]]) {
        [self.backgroundView removeObserver:self forKeyPath:kNoDataViewObserveKeyPath context:nil];
    }
}

- (void)replace_dealloc {
    [self freeNoDataViewIfNeeded];
    [self replace_dealloc];
    NSLog(@"TableView 视图正常销毁");
}


@end

 

 

UICollectionView+NoDataView.m

#import "UICollectionView+NoDataView.h"
#import <objc/runtime.h>
#import "NoDataView.h"

/**
 消除警告
 */
@protocol CollectionViewDelegate <NSObject>
@optional
- (UIView   *)noDataView;
- (UIImage  *)noDataViewImage;
- (NSString *)noDataViewMessage;
- (UIColor  *)noDataViewMessageColor;
- (NSNumber *)noDataViewCenterYOffset;
@end

@implementation UICollectionView (NoDataView)
/**
 加载时, 交换方法
 */
+ (void)load {
    //  只交换一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        Method reloadData    = class_getInstanceMethod(self, @selector(reloadData));
        Method replace_reloadData = class_getInstanceMethod(self, @selector(replace_reloadData));
        method_exchangeImplementations(reloadData, replace_reloadData);
        
        Method dealloc       = class_getInstanceMethod(self, NSSelectorFromString(@"dealloc"));
        Method replace_dealloc    = class_getInstanceMethod(self, @selector(replace_dealloc));
        method_exchangeImplementations(dealloc, replace_dealloc);
    });
}

/**
 在 ReloadData 的时候检查数据
 */
- (void)replace_reloadData {
    
    [self replace_reloadData];
    
    //  忽略第一次加载
    if (![self isInitFinish]) {
        [self havingData:YES];
        [self setIsInitFinish:YES];
        return ;
    }
    //  刷新完成之后检测数据量
    dispatch_async(dispatch_get_main_queue(), ^{
        
        NSInteger numberOfSections = [self numberOfSections];
        BOOL havingData = NO;
        for (NSInteger i = 0; i < numberOfSections; i++) {
            if ([self numberOfItemsInSection:i] > 0) {
                havingData = YES;
                break;
            }
        }
        
        [self havingData:havingData];
    });
}

/**
 展示占位图
 */
- (void)havingData:(BOOL)havingData {
    
    //  不需要显示占位图
    if (havingData) {
        [self freeNoDataViewIfNeeded];
        self.backgroundView = nil;
        return ;
    }
    
    //  不需要重复创建
    if (self.backgroundView) {
        return ;
    }
    
    //  自定义了占位图
    if ([self.delegate respondsToSelector:@selector(noDataView)]) {
        self.backgroundView = [self.delegate performSelector:@selector(noDataView)];
        return ;
    }
    
    //  使用自带的
    UIImage  *img   = nil;
    NSString *msg   = @"暂无数据";
    UIColor  *color = [UIColor lightGrayColor];
    CGFloat  offset = 0;
    
    //  获取图片
    if ([self.delegate    respondsToSelector:@selector(noDataViewImage)]) {
        img = [self.delegate performSelector:@selector(noDataViewImage)];
    }
    //  获取文字
    if ([self.delegate    respondsToSelector:@selector(noDataViewMessage)]) {
        msg = [self.delegate performSelector:@selector(noDataViewMessage)];
    }
    //  获取颜色
    if ([self.delegate      respondsToSelector:@selector(noDataViewMessageColor)]) {
        color = [self.delegate performSelector:@selector(noDataViewMessageColor)];
    }
    //  获取偏移量
    if ([self.delegate        respondsToSelector:@selector(noDataViewCenterYOffset)]) {
        offset = [[self.delegate performSelector:@selector(noDataViewCenterYOffset)] floatValue];
    }
    
    //  创建占位图
    self.backgroundView = [self defaultNoDataViewWithImage  :img message:msg color:color offsetY:offset];
}

/**
 默认的占位图
 */
- (UIView *)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);
    
    //  视图
    NoDataView * view   = [[NoDataView alloc] init];
    [view addSubview:imgView];
    [view addSubview:label];
    
    //  实现跟随 collectionView 滚动
    [view addObserver:self forKeyPath:kNoDataViewObserveKeyPath options:NSKeyValueObservingOptionNew context:nil];
    return view;
}


/**
 监听
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:kNoDataViewObserveKeyPath]) {
        
        /**
         在 collectionView 滚动 ContentOffset 改变时, 会同步改变 backgroundView 的 frame.origin.y
         可以实现, backgroundView 位置相对于 collectionView 不动, 但是我们希望
         backgroundView 跟随 collectionView 的滚动而滚动, 只能强制设置 frame.origin.y 永远为 0
         兼容 MJRefresh
         */
        CGRect frame = [[change objectForKey:NSKeyValueChangeNewKey] CGRectValue];
        if (frame.origin.y != 0) {
            frame.origin.y  = 0;
            self.backgroundView.frame = frame;
        }
    }
}

#pragma mark - 属性

/// 加载完数据的标记属性名
static NSString * const kCollectionViewPropertyInitFinish = @"kCollectionViewPropertyInitFinish";

/**
 设置已经加载完成数据了
 */
- (void)setIsInitFinish:(BOOL)finish {
    objc_setAssociatedObject(self, &kCollectionViewPropertyInitFinish, @(finish), OBJC_ASSOCIATION_ASSIGN);
}

/**
 是否已经加载完成数据
 */
- (BOOL)isInitFinish {
    id obj = objc_getAssociatedObject(self, &kCollectionViewPropertyInitFinish);
    return [obj boolValue];
}

/**
 移除 KVO 监听
 */
- (void)freeNoDataViewIfNeeded {
    
    if ([self.backgroundView isKindOfClass:[NoDataView class]]) {
        [self.backgroundView removeObserver:self forKeyPath:kNoDataViewObserveKeyPath context:nil];
    }
}

- (void)replace_dealloc {
    [self freeNoDataViewIfNeeded];
    [self replace_dealloc];
    NSLog(@"CollectionView 视图正常销毁");
}
@end

 

 

NoDataView.h

#import <UIKit/UIKit.h>

extern NSString * const kNoDataViewObserveKeyPath;

@interface NoDataView : UIView

@end

NoDataView.m

#import "NoDataView.h"
NSString * const kNoDataViewObserveKeyPath = @"frame";
@implementation NoDataView

- (void)dealloc {
    NSLog(@"占位视图正常销毁");
}

@end

 

调用

#import "ViewController.h"
#import "MJRefresh.h"

@interface ViewController () <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) UITableView * tableView;
@property (nonatomic, strong) NSMutableArray * dataArr;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
    self.tableView.delegate = self;
    self.tableView.dataSource = self;
    [self.view addSubview:self.tableView];
    self.tableView.tableFooterView = [UIView new];
    
    
    __weak typeof(self) weakSelf = self;
    self.tableView.mj_header  = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
        [weakSelf loadData];
    }];
}

- (void)loadData {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.7 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self.tableView.mj_header endRefreshing];
        [self.tableView reloadData];
    });
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    return [UITableViewCell new];
}


#pragma mark - TableView 占位图

- (UIImage *)noDataViewImage {
    return [UIImage imageNamed:@"note_list_no_data"];
}

- (NSString *)noDataViewMessage {
    return @"都用起来吧, 起飞~";
}

- (UIColor *)noDataViewMessageColor {
    return [UIColor blackColor];
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}


@end

 

posted @ 2018-01-02 15:19  FMDN  阅读(1739)  评论(0编辑  收藏  举报