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