CXTableView

UITableView轻量级封装


License
MIT
Install
pod try CXTableView

Documentation

CXTableView

CI Status Version License Platform

项目中MVC架构存在的问题

效果

Installation

CXTableView is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'CXTableView'

License

CXTableView is available under the MIT license. See the LICENSE file for more info.

问题阐述

  • VC过于臃肿 业务稍微复杂点就代码量1000+
  • View与Model之间耦合性太强
  • bug不易定位
  • 业务更改维护成本很高

基于这些问题我们就以一个UITableView的列表来进行阐述首先要弄明白 MVC 的核心:控制器(以下简称 C)负责模型(以下简称 M)和视图(以下简称 V)的交互。

这里所说的 M,通常不是一个单独的类,很多情况下它是由多个类构成的一个层。最上层的通常是以 Model 结尾的类,它直接被 C 持有。

项目版本

在 C 中,我们创建 UITableView 对象,然后将它的数据源和代理设置为自己。也就是自己管理着 UI 逻辑和数据存取的逻辑。在这种架构下,主要存在这些问题:

  • 违背 MVC 模式,现在是 V 持有 C 和 M。
  • C 做了全部逻辑,耦合太严重。

DataSource

首先实现一个tableView 我们要实现两个数据源方法

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
```### Delegate
这里包含一些点击代理,高度返回以及一系列的头部尾部视图的配置,以及`cell`绘制UI的操作
```objc
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;

进阶版本

首先的思路是单独把数据源方法抽离出去,单独实现 这里我打算用到一个遵循UITableViewDataSource的协议和一个单独的数据源类(需要遵循自定义的协议)来实现 结合前面数遇见的传统DataSource问题我们可以思考下这个协议api应该怎么设计

@protocol CXTableViewDataSourceProtocol <UITableViewDataSource>
@optional

- (Class)tableView:(UITableView*)tableView cellClassForObject:(id)object;
- (UITableViewCell *)registerTableView:(UITableView*)tableView cellClassForObject:(id)object;
- (id)tableView:(UITableView *)tableView objectForRowAtIndexPath:(NSIndexPath *)indexPath;
- (CGFloat)rowHeightForObject:(id)object;

@end
  • 拿带数据源做对应的事
  • 注册cell类型支持多种样式的cell
  • 配置cell的高度

再自定义一个遵循CXTableViewDataSourceProtocol的数据源类

@interface CXTableViewDataSource : NSObject<CXTableViewDataSourceProtocol>

/**
包装setion 是个二维数组
*/
@property (nonatomic, strong) NSMutableArray <CXTableViewSectionModel *>*sections;

- (void)reamoveAllItems;

- (void)addItem:(id)item;

- (void)addItem:(id)item section:(NSInteger)section;

- (id)loadFromXib:(NSString *)class_name;

@end

内部只需要做好协议实现,部分协议方法以让子类实现的方式暴露给给外界调用,这里我包含了一个设置高度的协议在数据源方法里,这主要是考虑到用户在使用的时候,很多的时候我们的高度并不是固定的,理论上应该配置高度的地方是cell它自己本身,因为涉及UI,目前同样开放了cell设置高度的接口,但协议的高度接口优先级要高于cell配置的接口,使用者只需要在CXTableViewDataSource的子类中去处理数据

#pragma mark - CXTableViewDataSourceProtocol
- (id)tableView:(UITableView *)tableView objectForRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.sections.count > indexPath.section) {
CXTableViewSectionModel *tableViewSectionModel = [self.sections objectAtIndex:indexPath.section];
if ([tableViewSectionModel.items count] > indexPath.row) {
return [tableViewSectionModel.items objectAtIndex:indexPath.row];
}
}
return nil;
}

- (UITableViewCell *)registerTableView:(UITableView*)tableView cellClassForObject:(id)object {
//子类实现
Class cellClass = [self tableView:tableView cellClassForObject:object];
NSString *className = [NSString stringWithUTF8String:class_getName(cellClass)];
return [[cellClass alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:className];
}

- (Class)tableView:(UITableView*)tableView cellClassForObject:(id)object {
//子类实现
return [CXBaseTableViewCell class];
}

- (CGFloat)rowHeightForObject:(id)object {
//子类实现
return 0;
}

具体的代理全部交由CXTableViewDataSource来处理

#pragma mark - UITableViewDataSource Required
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (self.sections.count > section) {
CXTableViewSectionModel *tableViewSectionModel =  [self.sections objectAtIndex:section];
return tableViewSectionModel.items.count;
}
return 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
//通过获得数据来确定Cell的样式
id object = [self tableView:tableView objectForRowAtIndexPath:indexPath];
Class cellClass = [self tableView:tableView cellClassForObject:object];
NSString *className = [NSString stringWithUTF8String:class_getName(cellClass)];
CXBaseTableViewCell* cell = (CXBaseTableViewCell*)[tableView dequeueReusableCellWithIdentifier:className];
if (!cell) {
cell = (CXBaseTableViewCell *)[self registerTableView:tableView cellClassForObject:object];
}
return cell;
}

#pragma mark - UITableViewDataSource Optional
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.sections ? self.sections.count : 0;
}

现在dataSource的数据源代理已全部交由CXTableViewDataSource来处理

现在就只剩另外一个问题,那就是delegate的抽离,这里的处理方式稍微和dataSource的处理方式有些不同,dataSource的代理对象是一个我们自定义的CXTableViewDataSource对象,而delegate的代理对象,我们用CXTableView一个继承UITableView的子类,首先首先设置一个CXTableViewDelegateProtocol

@protocol CXTableViewDelegateProtocol <UITableViewDelegate>

@optional

- (void)didSelectObject:(id)object atIndexPath:(NSIndexPath*)indexPath;

@end

CXTableViewDelegateProtocol协议是继承UITableViewDelegate,之说以这样是为了好让CXTableView作为一个中间桥接的作用,在系统的代理上层去处理自己的业务的前提下不影响系统的代理,和runtime交换方法有点异曲同工之妙

//.h
@interface CXTableView : UITableView<UITableViewDelegate>

@property (nonatomic, weak) id<CXTableViewDataSourceProtocol> cxdataSource;
@property (nonatomic, weak) id<CXTableViewDelegateProtocol> cxdelegate;

@end
//.m
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
id<CXTableViewDataSourceProtocol> dataSource = (id<CXTableViewDataSourceProtocol>)self.dataSource;
id object = [dataSource tableView:tableView objectForRowAtIndexPath:indexPath];
/*
理论上现在已经知道了高度,但由于object是id类型
需要子类提前异步计算好返回
如果子类没有计算,则认为这里是固定高度,可由Cell自己配置
*/
Class cls = [dataSource tableView:tableView cellClassForObject:object];
return [dataSource rowHeightForObject:object] > 0?[dataSource rowHeightForObject:object]:[cls rowHeightForItem:object];
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.cxdelegate && [self.cxdelegate respondsToSelector:@selector(didSelectObject:atIndexPath:)]) {
id<CXTableViewDataSourceProtocol> dataSource = (id<CXTableViewDataSourceProtocol>)self.dataSource;
id object = [dataSource tableView:tableView objectForRowAtIndexPath:indexPath];
[self.cxdelegate didSelectObject:object atIndexPath:indexPath];
} else if([self.cxdelegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]) {
[self.cxdelegate tableView:tableView didSelectRowAtIndexPath:indexPath];
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
id<CXTableViewDataSourceProtocol> dataSource = (id<CXTableViewDataSourceProtocol>)self.dataSource;
id object = [dataSource tableView:tableView objectForRowAtIndexPath:indexPath];
//给cell 绑定数据
[(CXBaseTableViewCell *)cell setItem:object];
if ([self.cxdelegate respondsToSelector:@selector(tableView:willDisplayCell:forRowAtIndexPath:)]) {
[self.cxdelegate tableView:tableView willDisplayCell:cell forRowAtIndexPath:indexPath];
}
}

- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section {
if([self.cxdelegate respondsToSelector:@selector(tableView:willDisplayHeaderView:forSection:)]) {
[self.cxdelegate tableView:tableView willDisplayHeaderView:view forSection:section];
}
}

- (void)tableView:(UITableView *)tableView willDisplayFooterView:(UIView *)view forSection:(NSInteger)section {
if([self.cxdelegate respondsToSelector:@selector(tableView:willDisplayFooterView:forSection:)]) {
[self.cxdelegate tableView:tableView willDisplayFooterView:view forSection:section];
}
}

// 后续还可以继续添加代理 或者自己定义子类去实现 中转传递

另外给cell 绑定数据的逻辑我放在了- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath 方法里,而并没有放在- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 方法中 这是因为cellForRowAtIndexPath做的事情其实就在绑定Cell类型,而willDisplayCell方法中是Cell已经绘制出来了,这会添加数据再合适不过,包括前面提到的Cell设置高度接口这里也做了对应的说明 至此,对CXTableView的基本封装就完成了,但远不仅仅是这些

  • 下拉刷新
  • 空白页
  • 带动画的loading页(知乎,简书那种)
  • 等等...

这些东西都是业务开发中比较常见的场景 此篇文章中这里就没有进一步封装了(时间问题)思想最重要,要明白为什么这么去抽离,面向接口协议开发

完结版本

基于使用层面的考虑, 我们实现一个UIViewController的子类,并且把数据源和代理封装到 C 中

@class CXTableViewDataSource;
NS_ASSUME_NONNULL_BEGIN
@protocol CXTableViewControllerDelegate <NSObject>

@required
- (void)configCXDataSource;
@optional
- (void)configCXDelegate;
@end

@interface CXTableViewController : UIViewController<CXTableViewDelegateProtocol,CXTableViewControllerDelegate>

@property (nonatomic, strong) CXTableView *tableView;
@property (nonatomic, strong) CXTableViewDataSource *tableViewDataSource;
@property (nonatomic, assign) UITableViewStyle tableViewStyle;

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)initWithStyle:(UITableViewStyle)style;

@end
NS_ASSUME_NONNULL_END

用户使用只要做几件事就行

  • 首先你需要创建一个继承自 CXTableViewController 的视图控制器,并且调用它的 initWithStyle## 方法。

  • 实现CXTableViewDataSource的子类

@implementation CXDemoDataSource

- (void)loadData {
//业务数据处理  这里还可以抽离出一个p层
NSArray *cellIdentifers = @[NSStringFromClass([CXDemo1TableViewCell class]),NSStringFromClass([CXDemoTableViewCell class])];
NSMutableArray *items = [@[] mutableCopy];
id<ContentViewAdapterProtocol> demoAdapter;
for (NSInteger i = 0; i < 40; i ++) {
if (i%5 > 2) {
CXDemoItem *item = [CXDemoItem new];
item.cellIdentifier = cellIdentifers[i%2];
item.rowHeight = 150;
item.name = item.cellIdentifier;
item.subName = [NSString stringWithFormat:@"%zd",i];
demoAdapter = [[CXDemoAdapter alloc] initWithData:item];
} else{
CXDemo1Item *item = [CXDemo1Item new];
item.identifier = cellIdentifers[i%2];
item.rowHeight = 70;
item.contentName = item.identifier;
item.titleName = [NSString stringWithFormat:@"%zd",i];
demoAdapter = [[CXDemoAdapter alloc] initWithData:item];
}
[items addObject:demoAdapter];
}
CXTableViewSectionModel *sectionModel = [[CXTableViewSectionModel alloc] initWithItemArray:items];
self.sections = [NSMutableArray arrayWithObject:sectionModel];
}

#pragma mark - CXTableViewDataSourceProtocol
//注册cell类型
- (UITableViewCell *)registerTableView:(UITableView*)tableView cellClassForObject:(id<ContentViewAdapterProtocol>)object {
return [self loadFromXib:object.cellIdentifier];
}

//确立Cell的类型
- (Class)tableView:(UITableView *)tableView cellClassForObject:(id<ContentViewAdapterProtocol>)object {
return [NSClassFromString(object.cellIdentifier) class];
}

//异步计算好高度
- (CGFloat)rowHeightForObject:(id<ContentViewAdapterProtocol>)object {
return object.rowHeight;
}

VC的调用就更简单了 在这里我把 self.tableView.cxdelegate = self.demoTableViewDelegate;

@interface CXDemoViewController ()

@property (nonatomic, strong) UIActivityIndicatorView *activityIndicatorView ;
@property (strong, nonatomic) CXDemoDataSource *demoDataSource;
@property (strong, nonatomic) CXDemoTableViewDelegate *demoTableViewDelegate;

@end

@implementation CXDemoViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.title = @"CXTableView";
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithTitle:@"数据加载失败" style:UIBarButtonItemStylePlain target:self action:@selector(failClick)];
[self.navigationItem setRightBarButtonItem:item];
self.tableView.isNeedPullUpToRefresh = YES;
self.tableView.isNeedPullDownToRefresh = YES;
self.tableView.autoPullDownToRefresh = YES;
}

#pragma mark - CXTableViewControllerDelegate
- (void)configCXDataSource {
//设置数据源
self.tableViewDataSource = self.demoDataSource;
}

- (void)configCXDelegate {
//设置代理
self.tableView.cxdelegate = self.demoTableViewDelegate;
}

#pragma mark - action
- (void)failClick {
[self.tableViewDataSource reamoveAllItems];
[self.tableView reloadData];
}

#pragma mark - set&get
- (CXDemoDataSource *)demoDataSource{
if (!_demoDataSource) {
_demoDataSource = [[CXDemoDataSource alloc] init];
}
return _demoDataSource;
}

- (CXDemoTableViewDelegate *)demoTableViewDelegate{
if (!_demoTableViewDelegate) {
_demoTableViewDelegate = [[CXDemoTableViewDelegate alloc] init];
_demoTableViewDelegate.tableView = self.tableView;
_demoTableViewDelegate.demoDataSource = self.demoDataSource;
}
return _demoTableViewDelegate;
}

@end

self.demoTableViewDelegate 就比较贴近各自的项目逻辑

到目前为止,我们实现了对UITableView以及相关协议、方法的封装,使它更容易使用,避免了很多重复、无意义的代码。 M 只关心数据 C 只负责调度 配置 V 只负责展示数据

self.demoTableViewDelegate 可以把一些复杂的业务逻辑,它直接和CXDemoDataSource通信 如果业务再复杂点,还可用self.demoTableViewDelegate的分类来处理业务分类

补充版本

  • 下拉刷新的封装

上次提到过的下拉刷新的封装,以及空白页的处理,因为这些从属性构造来讲,它们应该都属于TableView,所以我这边还是基于CXTableViewDelegateProtocol,给它增加协议方法

@protocol CXTableViewDelegateProtocol <UITableViewDelegate>

@optional

/**
cell点击的回调

@param object 对象
@param indexPath 索引
*/
- (void)didSelectObject:(id)object atIndexPath:(NSIndexPath*)indexPath;

/**
空白占位

@return 空白占位图
*/
- (UIView *)registerEmptyView;

/**
下拉刷新触发的方法
*/
- (void)pullDownToRefresh;

/**
上拉加载触发的方法
*/
- (void)pullUpToRefresh;

@end

再给CXTableView 增加4个属性 两个关闭动画的方法

@interface CXTableView : UITableView<UITableViewDelegate>

@property (nonatomic, weak) id<CXTableViewDataSourceProtocol> cxdataSource;
@property (nonatomic, weak) id<CXTableViewDelegateProtocol> cxdelegate;

@property (nonatomic, assign) BOOL isNeedPullDownToRefresh;
@property (nonatomic, assign) BOOL isNeedPullUpToRefresh;
@property (assign, nonatomic) BOOL autoPullDownToRefresh;
@property (assign, nonatomic) BOOL loadCompleted;

- (void)stopRefreshingAnimation;
- (void)triggerRefreshing;

@end

这边下拉刷新控件我选择的是SVPullToRefresh比较轻量级,其中内部有两个BUG,在源码层级上给它做了修改

#import "UIScrollView+SVPullToRefresh.h"
- (void)startAnimating{
switch (self.position) {
case SVPullToRefreshPositionTop:
//bug 修复 设置了偏移量后 不能自动刷新的问题
if(fequalzero(self.scrollView.contentOffset.y) + self.originalTopInset) {
[self.scrollView setContentOffset:CGPointMake(self.scrollView.contentOffset.x, - self.frame.size.height - self.originalTopInset) animated:YES];
self.wasTriggeredByUser = NO;
}
else
self.wasTriggeredByUser = YES;

break;
case SVPullToRefreshPositionBottom:

if((fequalzero(self.scrollView.contentOffset.y) && self.scrollView.contentSize.height < self.scrollView.bounds.size.height)
|| fequal(self.scrollView.contentOffset.y, self.scrollView.contentSize.height - self.scrollView.bounds.size.height)) {
[self.scrollView setContentOffset:(CGPoint){.y = MAX(self.scrollView.contentSize.height - self.scrollView.bounds.size.height, 0.0f) + self.frame.size.height} animated:YES];
self.wasTriggeredByUser = NO;
}
else
self.wasTriggeredByUser = YES;

break;
}
self.state = SVPullToRefreshStateLoading;
}
#import "UIScrollView+SVInfiniteScrolling.h"
id customView = [self.viewForState objectAtIndex:newState];
BOOL hasCustomView = [customView isKindOfClass:[UIView class]];

if(hasCustomView) {
[self addSubview:customView];
CGRect viewBounds = [customView bounds];
CGPoint origin = CGPointMake(roundf((self.bounds.size.width-viewBounds.size.width)/2), roundf((self.bounds.size.height-viewBounds.size.height)/2));
[customView setFrame:CGRectMake(origin.x, origin.y, viewBounds.size.width, viewBounds.size.height)];
//bug 解决设置了自定义View SVInfiniteScrollingStateStopped状态后的 菊花不消失的问题
switch (newState) {
case SVInfiniteScrollingStateStopped:
[self.activityIndicatorView stopAnimating];
break;
}
}

原有的下拉刷新箭头不精细,比较喜欢简书那样的下拉刷新,所有就画过了一个箭头

#pragma mark - SVPullToRefreshArrow

@implementation SVPullToRefreshArrow
@synthesize arrowColor;

- (UIColor *)arrowColor {
if (arrowColor) return arrowColor;
return [UIColor lightGrayColor]; // default Color
}

- (void)drawRect:(CGRect)rect {
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextMoveToPoint(c, 11, 20);
CGContextAddLineToPoint(c, 11, 35);
CGContextMoveToPoint(c, 6, 30);
CGContextAddLineToPoint(c, 11, 35);
CGContextAddLineToPoint(c, 16, 30);
CGContextSetLineWidth(c, 0.8);
CGContextSetStrokeColorWithColor(c, self.arrowColor.CGColor);
CGContextSetLineCap(c, kCGLineCapRound);
CGContextDrawPath(c, kCGPathStroke);
}

@end

使用层面很简单, 后续如果想换MJ也可以改CXTableView的实现,外界调用不用动

- (void)pullDownToRefresh {
//模拟网络请求
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.demoDataSource loadData];
[self.tableView setLoadCompleted:NO];
[self.tableView stopRefreshingAnimation];
[self.tableView reloadData];
});
}


- (void)pullUpToRefresh {
//模拟网络请求
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.demoDataSource loadMoreData];
[self.tableView setLoadCompleted:YES];
[self.tableView triggerRefreshing];
[self.tableView reloadData];
});
}
  • 空白页的封装

主要是针对CXTableView实现了一个CXTableView+CXEmpty分类 主要思路是针对CXTableView 刷新数据方法进行交换

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[[self class] cx_swizzleClassMethodWithOriginalSel:@selector(reloadData) newSel:@selector(cx_reloadData)];
[[self class] cx_swizzleClassMethodWithOriginalSel:@selector(insertSections:withRowAnimation:) newSel:@selector(cx_insertSections:withRowAnimation:)];
[[self class] cx_swizzleClassMethodWithOriginalSel:@selector(insertRowsAtIndexPaths:withRowAnimation:) newSel:@selector(cx_insertRowsAtIndexPaths:withRowAnimation:)];
[[self class] cx_swizzleClassMethodWithOriginalSel:@selector(deleteSections:withRowAnimation:) newSel:@selector(cx_deleteSections:withRowAnimation:)];
[[self class] cx_swizzleClassMethodWithOriginalSel:@selector(deleteRowsAtIndexPaths:withRowAnimation:) newSel:@selector(cx_deleteRowsAtIndexPaths:withRowAnimation:)];
});
}

- (void)cx_reloadData {
[self cx_reloadData];
//忽略第一次加载
if (![self isInitFinish]) {
[self setIsInitFinish:YES];
return;
}
[self checkData];
}

- (void)checkData {
dispatch_async(dispatch_get_main_queue(), ^{
if (!self.emptyView) {
return;
}
NSInteger sections = 1;
if ([self.cxdataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) {
sections = [self.cxdataSource numberOfSectionsInTableView:self];
}
if (sections == 0){
[self.emptyView removeFromSuperview];
[self addSubview:self.emptyView];
}else {
if (sections == 1){
NSInteger rowNumber = [self.cxdataSource tableView:self numberOfRowsInSection:0];
if (rowNumber == 0){
[self.emptyView removeFromSuperview];
[self addSubview:self.emptyView];
} else {
[self.emptyView removeFromSuperview];
}
}else {
[self.emptyView removeFromSuperview];
}
}
});
}

static NSString *const CXRegisterEmptyViewKey = @"CXRegisterEmptyViewKey";
static NSString *const CXTableViewPropertyInitFinishKey = @"CXTableViewPropertyInitFinishKey";

- (UIView *)emptyView {
if ([self.cxdelegate respondsToSelector:@selector(registerEmptyView)]) {
if (!objc_getAssociatedObject(self, &CXRegisterEmptyViewKey)) {
objc_setAssociatedObject(self, &CXRegisterEmptyViewKey, [self.cxdelegate registerEmptyView], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return objc_getAssociatedObject(self, &CXRegisterEmptyViewKey);
}
return nil;
}

- (void)setIsInitFinish:(BOOL)finish{
objc_setAssociatedObject(self, &CXTableViewPropertyInitFinishKey, @(finish), OBJC_ASSOCIATION_ASSIGN);
}

- (BOOL)isInitFinish{
id obj = objc_getAssociatedObject(self, &CXTableViewPropertyInitFinishKey);
return [obj boolValue];
}

空白页的配置完全交给了外界,只要实现代理即可,这里还做了一个首次拿数据的时候不检测空白页,因为tableView在一开始不配置数据的时候,就会主动触发一次reloadData方法

demo地址:轻量级UITableView的封装

参照链接: 如何写好一个 UITableView