使用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 

四、演示

posted @ 2019-10-12 14:07  XYQ全哥  阅读(842)  评论(0编辑  收藏  举报