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