如何写UITableView解决项目中MVC架构存在的问题

效果预览

CXTableView.gif

问题阐述

  • 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342