使用Runtime的hook技术为tableView实现一个空白缺省页
一、介绍
UITableView和UICollectionView是iOS开发最常用的控件,也是必不可少的控件,这两个控件基本能实现各种各样的界面样式。
它们都是通过代理模式监测数据源的有无对数据进行UI的展示或隐藏。
如果没有数据时,UITableView和UICollectionView可能会展示了一个空白的页面,没有任何提示,逻辑上是没有问题的,但是对于用户而言,显得不够友好。
此时,最好做一个优化,也即没有数据时,刷新列表后提供一个缺省页。
给UITableView和UICollectionView添加一个缺省页,实现的方式有很多种,在这里,我首推采用运行时的hook技术来实现,第三方框架DZNEmptyDataSet就是用这个技术。
本文以UITableView为例,UICollectionView实现原理相同。
二、思路
(1)自定义一个缺省页视图NoDataEmptyView,添加图片视图UIImageView和提示文字标签label;
(2)给UITableView创建一个分类UITableView (ReloadData);
(3)在UITableView (ReloadData)分类中的load方法中使用hook技术用自定义的xyq_reloadData方法交换系统的reloadData方法;
(4)在UITableView (ReloadData)分类中使用关联对象的技术关联缺省页视图NoDataEmptyView对象,也即作为属性;
(5)在UITableView (ReloadData)分类中的xyq_reloadData方法中添加缺省页显示或隐藏的逻辑;
(6)在ViewController中刷新数据时正常调用reloadData方法即可达到实现。
三、代码
NoDataEmptyView
// NoDataEmptyView.h // 运行时 // // Created by 夏远全 on 2019/10/11. // Copyright © 2019 北京华樾教育科技有限公司. All rights reserved. // #import <UIKit/UIKit.h> NS_ASSUME_NONNULL_BEGIN @interface NoDataEmptyView : UIView @end NS_ASSUME_NONNULL_END
// // NoDataEmptyView.m // 运行时 // // Created by 夏远全 on 2019/10/11. // Copyright © 2019 北京华樾教育科技有限公司. All rights reserved. // #import "NoDataEmptyView.h" @interface NoDataEmptyView () @property (nonatomic, strong) UIImageView *imageView; @property (nonatomic, strong) UILabel *label; @end @implementation NoDataEmptyView -(instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setup]; } return self; } -(void)setup { self.backgroundColor = [[UIColor lightGrayColor] colorWithAlphaComponent:0.6]; self.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; self.imageView.image = [UIImage imageNamed:@"empty_body_kong"]; self.imageView.center = self.center; self.label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 100, 40)]; self.label.text = @"暂无数据"; self.label.textColor = [UIColor whiteColor]; self.label.textAlignment = NSTextAlignmentCenter; self.label.center = CGPointMake(self.imageView.center.x, CGRectGetMaxY(self.imageView.frame)+40); [self addSubview:self.imageView]; [self addSubview:self.label]; } @end
UITableView (ReloadData)
// // UITableView+ReloadData.h // 运行时 // // Created by 夏远全 on 2019/10/11. // Copyright © 2019 北京华樾教育科技有限公司. All rights reserved. // #import <UIKit/UIKit.h> #import "NoDataEmptyView.h" NS_ASSUME_NONNULL_BEGIN @interface UITableView (ReloadData) @property (nonatomic, strong) NoDataEmptyView *emptyView; @end NS_ASSUME_NONNULL_END
// // UITableView+ReloadData.m // 运行时 // // Created by 夏远全 on 2019/10/11. // Copyright © 2019 北京华樾教育科技有限公司. All rights reserved. // #import "UITableView+ReloadData.h" #import <objc/message.h> static NSString *const NoDataEmptyViewKey = @"NoDataEmptyViewKey"; @implementation UITableView (ReloadData) #pragma mark - 交换方法 hook +(void)load { Method originMethod = class_getInstanceMethod(self, @selector(reloadData)); Method currentMethod = class_getInstanceMethod(self, @selector(xyq_reloadData)); static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ method_exchangeImplementations(originMethod, currentMethod); }); } #pragma mark - 刷新数据 -(void)xyq_reloadData { [self xyq_reloadData]; [self fillEmptyView]; } #pragma mark - 填充空白页 -(void)fillEmptyView { NSInteger sections = 1; NSInteger rows = 0; id <UITableViewDataSource> dataSource = self.dataSource; if ([dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) { sections = [dataSource numberOfSectionsInTableView:self]; if ([dataSource respondsToSelector:@selector(tableView:numberOfRowsInSection:)]) { for (int i=0; i<sections; i++) { rows += [dataSource tableView:self numberOfRowsInSection:i]; } } } if (rows == 0) { if (![self.subviews containsObject:self.emptyView]) { self.emptyView = [[NoDataEmptyView alloc] initWithFrame:self.bounds]; [self addSubview:self.emptyView]; } } else{ [self.emptyView removeFromSuperview]; } } #pragma mark - 关联对象 -(void)setEmptyView:(NoDataEmptyView *)emptyView { objc_setAssociatedObject(self, &NoDataEmptyViewKey, emptyView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } -(NoDataEmptyView *)emptyView { return objc_getAssociatedObject(self, &NoDataEmptyViewKey); } @end
DataTableViewController
// // DataTableViewController.m // 运行时 // // Created by 夏远全 on 2019/10/11. // Copyright © 2019 北京华樾教育科技有限公司. All rights reserved. // #import "DataTableViewController.h" #import "UITableView+ReloadData.h" @interface DataTableViewController ()<UITableViewDataSource> @property (nonatomic, strong) UITableView *tableView; @property (nonatomic, strong) UIButton *haveDataBtn; @property (nonatomic, strong) UIButton *clearDataBtn; @property (nonatomic, strong) NSMutableArray *dataSource; @end @implementation DataTableViewController #pragma mark - life cycle - (void)viewDidLoad { [super viewDidLoad]; [self setupNavigation]; [self setupSubviews]; } -(void)setupNavigation { self.title = @"测试空白页"; self.view.backgroundColor = [UIColor whiteColor]; } -(void)setupSubviews { [self.view addSubview:self.haveDataBtn]; [self.view addSubview:self.clearDataBtn]; [self.view addSubview:self.tableView]; } #pragma mark - dataSource methods -(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.dataSource.count; } -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ static NSString *reuserIdentifier = @"cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuserIdentifier]; if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuserIdentifier]; } cell.textLabel.text = self.dataSource[indexPath.row]; return cell; } #pragma mark - event -(void)haveDataBtnAction { self.dataSource = @[@"第1行数据",@"第2行数据",@"第3行数据",@"第4行数据",@"第5行数据",@"第6行数据"].mutableCopy; [self.tableView reloadData]; //内部调用自己的xyq_reloadData } -(void)clearDataBtnAction { [self.dataSource removeAllObjects]; [self.tableView reloadData]; //内部调用自己的xyq_reloadData } #pragma mark - getters -(UITableView *)tableView { if (!_tableView) { CGFloat width = [UIScreen mainScreen].bounds.size.width; CGFloat height = [UIScreen mainScreen].bounds.size.height; _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 64+50, width, height-64-50)]; _tableView.backgroundColor = [UIColor whiteColor]; _tableView.tableFooterView = [[UIView alloc] init]; _tableView.dataSource = self; } return _tableView; } -(UIButton *)haveDataBtn { if (!_haveDataBtn) { CGFloat width = [UIScreen mainScreen].bounds.size.width; _haveDataBtn = [[UIButton alloc] initWithFrame:CGRectMake(10, 10+64, width/2-20, 30)]; _haveDataBtn.backgroundColor = [UIColor redColor]; [_haveDataBtn setTitle:@"显示数据" forState:UIControlStateNormal]; [_haveDataBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; [_haveDataBtn addTarget:self action:@selector(haveDataBtnAction) forControlEvents:UIControlEventTouchUpInside]; } return _haveDataBtn; } -(UIButton *)clearDataBtn { if (!_clearDataBtn) { CGFloat width = [UIScreen mainScreen].bounds.size.width; _clearDataBtn = [[UIButton alloc] initWithFrame:CGRectMake(width/2+10, 10+64, width/2-20, 30)]; _clearDataBtn.backgroundColor = [UIColor purpleColor]; [_clearDataBtn setTitle:@"清空数据" forState:UIControlStateNormal]; [_clearDataBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; [_clearDataBtn addTarget:self action:@selector(clearDataBtnAction) forControlEvents:UIControlEventTouchUpInside]; } return _clearDataBtn; } @end
四、演示