CXTableView 0.1.3

CXTableView 0.1.3

Maintained by caixiang305621856.



  • By
  • caixiang305621856

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