封装基类tableViewController来提升开发效率
列表类视图在开发中应该算是应用最多的了,为了减少不必要的代码量,避免重复造轮子,我在这里封装了一个基类的WMBaseTableViewController。
WMBaseTableViewController里面集合了刷新加载/空数据显示处理/网络状态异常显示处理,只需要传入接口url以及入参param 然后完成tableView代理就会生成一个完整的列表视图界面。可以让我们更关注于业务的逻辑而不用重复做冗余的代码工作。
1.WMBaseTableVC的封装
在.h里暴露出子类可能会用到的设置参数,包括:tableViewFrame
,tableViewStyle
,含page的apiParam的block
,apiManager的block
,是否要刷新加载isHasRefresh/isHasMore
,一次请求的pageSize
等,再暴露出子类可调用的刷新方法
以满足在标签类视图切换tab时等情况进行刷新使用,还有请求的数据解析方法暴露出来 且必须重写
。
代码如下:
typedef NS_ENUM(NSUInteger , RequestNoDataType) {
/** 本身就是没有数据 */
RequestNoDataTypeNoData = 0,
/** 服务器错误导致没有数据 */
RequestNoDataTypeServerError = 1,
/** 网络原因导致没有数据 */
RequestNoDataTypeInternetError = 2,
};
@interface WMBaseTableViewController : BaseViewController
/** 数据源 */
@property (nonatomic, strong) NSMutableArray *dataArray;
/** 显示数据的tableView控件 */
@property (nonatomic, strong) UITableView *tableView;
/** tableview的frame,默认值请到init方法中查看,可在子类进行设置 */
@property (nonatomic, assign) CGRect tableViewFrame;
/** tableview的style,默认值请到init方法中查看,可在子类进行设置 */
@property (nonatomic, assign) UITableViewStyle tableViewStyle;
/** 含page的apiParam的block,可在子类设置,可以为nil */
@property (nonatomic, copy) NSDictionary * (^urlParamBlock)(NSUInteger page);
/** apiManager的block,可在子类进行设置,且必须设置 */
@property (nonatomic, copy) WMBaseAPIManager * (^apiManagerBlock)(void);
/** 是否要刷新 */
@property(nonatomic,assign) BOOL isHasRefresh;
/** 是否要加载 */
@property(nonatomic,assign) BOOL isHasMore;
/** 一次请求的pageSize,默认值请到init方法中查看,可在子类进行设置 */
@property (nonatomic, assign) NSUInteger pageSize;
/** 当没有数据,且没有正在进行网络请求时,需要显示的String,默认值请到init方法中查看 */
@property (nonatomic, copy) NSString *noDataString;
/** 当没有数据,且没有正在进行网络请求时,需要显示的图片,默认值请到init方法中查看 */
@property (nonatomic, copy) UIImage *noDataImg;
/** 无数据时的请求的状态设置 */
@property (nonatomic, assign) RequestNoDataType noDataType;
/**
* 对网络请求获取的json数据,解析成对应的array,需要rewrite
*/
- (NSArray *)parseToModelArrayWithResponseObj:(id)responseObj;
/**
* 对子类暴露的refreshData,用于子类调用,做规定动作以外的其他手动刷新
*/
- (void)refreshData;
- (void)refreshDataWithMJHeadRefresh;
/**
* 其他可拓展字段内容...
*/
复制代码
.m里声明不暴露外部的属性,包括:当前请求的pageIndex
,判断目前是否正在进行网络请求的flag
,刷新了的标志
等。
/** 当前请求的pageIndex */
@property (nonatomic, assign) NSUInteger currentPageIndex;
/** 判断目前是否正在进行网络请求的flag */
@property (nonatomic, assign, getter = isLoading) BOOL loading;
/** 刷新了的标志 */
@property (nonatomic, assign) BOOL isPull;
复制代码
init时设置默认值:
- (instancetype)init {
self = [super init];
if (self) {
//设置默认值
_loading = YES;
self.isHasRefresh = YES;
self.isHasMore = YES;
self.currentPageIndex = 1;
self.pageSize = kDefaultPageSize;
self.tableViewFrame = CGRectMake(0, kSTATUSNAVHEIGHT, kScreen_width, kScreen_height-kSTATUSNAVHEIGHT);
self.tableViewStyle = UITableViewStylePlain;
self.noDataString = @"这里空空如也哎~";
self.noDataImg = [UIImage imageNamed:@"Image_empty_default"];
self.noDataType = RequestNoDataTypeNoData;
}
return self;
}
复制代码
viewDidload添加tableView相关设置:
- (void)viewDidLoad {
[super viewDidLoad];
if (@available(iOS 11.0, *)) {
self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
self.tableView.estimatedRowHeight = 44.f;
} else {
self.automaticallyAdjustsScrollViewInsets = NO;
}
[self.view addSubview:self.tableView];
if (self.isHasRefresh) {
// 有下拉刷新功能
self.tableView.mj_header = [MJWeiMaiHeader headerWithRefreshingTarget:self refreshingAction:@selector(refreshData)];
[self.tableView.mj_header beginRefreshing];
}else {
// 无下拉刷新功能
[self refreshData];
}
if (self.isHasMore) {
// 有上拉加载功能 - 则执行
MJWeiMaiFooter *footer = [MJWeiMaiFooter footerWithRefreshingTarget:self refreshingAction:@selector(loadMoreData)];
self.tableView.mj_footer = footer;
[footer setTitle:@"已经到底咯~" forState:MJRefreshStateNoMoreData];
}
}
<!--lazy Load-->
- (UITableView *)tableView {
if (!_tableView) {
_tableView = [[UITableView alloc] initWithFrame:self.tableViewFrame style:self.tableViewStyle];
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.backgroundColor = [UIColor weimaiBackgroundColor];
_tableView.emptyDataSetSource = self;
_tableView.emptyDataSetDelegate = self;
_tableView.tableFooterView = [UIView new];
}
return _tableView;
}
- (NSMutableArray *)dataArray {
if (!_dataArray) {
_dataArray = [[NSMutableArray alloc] initWithCapacity:1];
}
return _dataArray;
}
复制代码
网络请求:
- (void)loadContentData {
NSLog(@"urlParamBlock::%@", self.urlParamBlock(self.currentPageIndex));
NSLog(@"apiManagerBlock::%@", self.apiManagerBlock);
[self.apiManagerBlock() loadDataWithParams:self.urlParamBlock(self.currentPageIndex) withSuccess:^(NSURLSessionDataTask *task, id responseObject) {
if (self.isPull) {
[self.dataArray removeAllObjects];
}
// 请求成功设置类型
self.noDataType = RequestNoDataTypeNoData;
// 获取解析的model数组
NSArray *newArray = [self parseToModelArrayWithResponseObj:responseObject];
[self.dataArray addObjectsFromArray:newArray];
// 判断pageSize
if (newArray.count == self.pageSize) {
[self reloadDataByResetNoMoreData];
}else{
[self reloadDataByNoMoreData];
}
} withFailure:^(WMResponseResult *error) {
if ([[AppNetStatusMonitor monitor] isNetworkEnable]) {
//服务器原因设置类型
_noDataType = RequestNoDataTypeServerError;
}else{
//网络原因设置类型
_noDataType = RequestNoDataTypeInternetError;
}
[self reloadDataByNoMoreData];
}];
}
复制代码
私有方法:
#pragma mark ----------- private Method ------------
// 外部调用刷新
- (void)refreshDataWithMJHeadRefresh {
[self.tableView.mj_header beginRefreshing];
}
/**
* 初始或上拉刷新加载数据
*/
-(void)refreshData {
self.currentPageIndex = 1;
self.loading = YES;
self.isPull = YES;
[self loadContentData];
}
/**
* 加载更多数据
*/
-(void)loadMoreData {
if (self.dataArray.count == 0) return;
self.isPull = NO;
self.currentPageIndex++;
[self loadContentData];
}
/**
* 重新加载数据,并隐藏“没有更多数据”的标签
*/
-(void)reloadDataByResetNoMoreData {
self.loading = NO;
if (self.isHasRefresh) {
[self.tableView.mj_header endRefreshing];
}
if (self.isHasMore) {
[self.tableView.mj_footer setHidden:NO];
[self.tableView.mj_footer resetNoMoreData];
}
[self.tableView reloadData];
}
/**
* 重新加载数据,并显示“没有更多数据”的标签
*/
-(void)reloadDataByNoMoreData {
self.loading = NO;
if (self.isHasRefresh) {
[self.tableView.mj_header endRefreshing];
}
if (self.isHasMore) {
//如果数据为空,则隐藏掉mjfooter
[self.tableView.mj_footer setHidden: ([_dataArray count] == 0)];
[self.tableView.mj_footer endRefreshingWithNoMoreData];
}
[self.tableView reloadData];
}
复制代码
tableView代理,需在子类重写
#pragma mark ----------- tableView Delegate && DataSource , Need to rewrite-------------
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.dataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSAssert(false, @"Over ride in subclasses");
return nil;
}
- (NSArray *)parseToModelArrayWithResponseObj:(id)responseObj {
NSAssert(false, @"Over ride in subclasses");
return nil;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
return [UIView new];
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 0.01;
}
复制代码
更新加载状态,空数据reload :
/**
* 更新是否正在加载的状态,并且重新reload emptydataset
*
* @param loading 是否loading
*/
- (void)setLoading:(BOOL)loading
{
if (self.isLoading == loading) {
return;
}
_loading = loading;
[self.tableView reloadEmptyDataSet];
}
复制代码
下面是DZNEmptyDataSet的代理:
// 空数据时的显示标题
- (NSAttributedString *)titleForEmptyDataSet:(UIScrollView *)scrollView {
if (self.loading) {
return nil;
}
NSMutableDictionary *attributes = [NSMutableDictionary new];
NSString *text = self.noDataString;
UIFont *font = [UIFont systemFontOfSize:12.0];
UIColor *textColor = [UIColor colorFromHexRGB:@"333333"];
if (self.noDataType == RequestNoDataTypeNoData) {
text = self.noDataString;
}else if(self.noDataType == RequestNoDataTypeServerError) {
text = @"加载失败";
}else {
text = @"网络不见了,快检查一下吧";
}
if (stringIsEmpty(text)) return nil;
[attributes setObject:font forKey:NSFontAttributeName];
[attributes setObject:textColor forKey:NSForegroundColorAttributeName];
return [[NSAttributedString alloc] initWithString:text attributes:attributes];
}
// 空数据时的详细描述
- (NSAttributedString *)descriptionForEmptyDataSet:(UIScrollView *)scrollView {
NSString *text = nil;
UIFont *font = nil;;
UIColor *textColor = nil;
NSMutableDictionary *attributes = [NSMutableDictionary new];
NSMutableParagraphStyle *paragraph = [NSMutableParagraphStyle new];
paragraph.lineBreakMode = NSLineBreakByWordWrapping;
paragraph.alignment = NSTextAlignmentCenter;
if (stringIsEmpty(text)) return nil;
if (font) [attributes setObject:font forKey:NSFontAttributeName];
if (textColor) [attributes setObject:textColor forKey:NSForegroundColorAttributeName];
if (paragraph) [attributes setObject:paragraph forKey:NSParagraphStyleAttributeName];
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text attributes:attributes];
return attributedString;
}
// 空数据的显示Image
- (UIImage *)imageForEmptyDataSet:(UIScrollView *)scrollView {
if (self.isLoading) {
if (self.isPull) {
return [UIImage imageNamed:@"playLoading1"];
}
return nil;
}else {
UIImage *noDataImg;
if (self.noDataType == RequestNoDataTypeNoData) {
noDataImg = self.noDataImg;
}else if(self.noDataType == RequestNoDataTypeServerError) {
noDataImg = [UIImage imageNamed:@"Image_empty_default"];
}else {
noDataImg = [UIImage imageNamed:@"Image_error_network"];
}
return noDataImg;
}
}
// loading动画
- (CAAnimation *)imageAnimationForEmptyDataSet:(UIScrollView *)scrollView {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform"];
animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
animation.toValue = [NSValue valueWithCATransform3D: CATransform3DMakeRotation(M_PI_2, 0.0, 0.0, 1.0) ];
animation.duration = 0.25;
animation.cumulative = YES;
animation.repeatCount = MAXFLOAT;
return animation;
}
// 按钮的字体显示
- (NSAttributedString *)buttonTitleForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state {
if (self.loading) {
return nil;
}
NSString *text = @"点击重新加载";
UIFont *font = [UIFont systemFontOfSize:16.f];
UIColor *textColor = [UIColor colorFromHexRGB:@"333333"];
NSMutableDictionary *attributes = [NSMutableDictionary new];
if (stringIsEmpty(text)) return nil;
if (font) [attributes setObject:font forKey:NSFontAttributeName];
if (textColor) [attributes setObject:textColor forKey:NSForegroundColorAttributeName];
return [[NSAttributedString alloc] initWithString:text attributes:attributes];
}
// 按钮的背景图片
- (UIImage *)buttonBackgroundImageForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state {
return nil;
}
// 控件之间间隙 default 11
- (CGFloat)spaceHeightForEmptyDataSet:(UIScrollView *)scrollView {
return 10.f;
}
#pragma mark - DZNEmptyDataSetDelegate Methods
- (BOOL)emptyDataSetShouldAllowTouch:(UIScrollView *)scrollView {
return YES;
}
- (BOOL)emptyDataSetShouldAllowScroll:(UIScrollView *)scrollView {
return YES;
}
- (BOOL)emptyDataSetShouldAnimateImageView:(UIScrollView *)scrollView {
return self.isLoading;
}
// 空数据图片和标题是否开启点击
- (void)emptyDataSet:(UIScrollView *)scrollView didTapView:(UIView *)view {
[self refreshData];
}
- (void)emptyDataSet:(UIScrollView *)scrollView didTapButton:(UIButton *)button {
[self refreshData];
}
复制代码
封装的过程中,空数据显示用的是DZNEmptyDataSet第三方控件,刷新加载主要梳理逻辑即可,关键的一点是urlParamBlock和apiManagerBlock。
因为咱们项目里网络组件是每个接口都要继承WMBaseAPIManager,入参Param也需要用我之前封装的ApiParam转成字典进行,所以两个都是声明一个有返回值的block
,在子类重写的时候返回就可以了。
有一点需要注意:
// 获取解析的model数组
NSArray *newArray = [self parseToModelArrayWithResponseObj:responseObject];
复制代码
解析responseObject是以数组Array形式返回的,所以在遇到根据接口返回值显示比较复杂的业务时要注意使用。
不过一般列表式视图都是满足的。
2.如何使用
下面以一个资讯列表作为例子。 init里设置:
- (instancetype)init {
self = [super init];
if (self) {
self.apiManagerBlock = ^WMBaseAPIManager * _Nonnull{
WMNewsListsAPIManager *apiManager = [WMNewsListsAPIManager new];
return apiManager;
};
kWeakSelf
self.urlParamBlock = ^NSDictionary * _Nonnull(NSUInteger page) {
ApiParam *param = [ApiParam new];
[param setValue:[SharedData shareInstance].cityInfoModel.areaId forName:@"areaId"];
[param setValue:@"1" forName:@"channelType"];
[param setValue:[NSString stringWithFormat:@"%ld",(long)weakSelf.changeValue]
forName:@"channelValue"];
[param setValue:@"20" forName:@"pageSize"];
[param setValue:[NSString stringWithFormat:@"%ld",(long)page] forName:@"pageNo"];
return [param dictionary];
};
}
return self;
}
复制代码
viewDidLoad:
- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView registerClass:[WMHealthNewsNormalCell class]
forCellReuseIdentifier:NSStringFromClass([WMHealthNewsNormalCell class])];
[self.tableView registerClass:[WMHealthNewsBigPicCell class]
forCellReuseIdentifier:NSStringFromClass([WMHealthNewsBigPicCell class])];
[self.tableView registerClass:[WMHealthNewsMorePicCell class]
forCellReuseIdentifier:NSStringFromClass([WMHealthNewsMorePicCell class])];
[self.tableView registerClass:[WMHealthNewsVideoCell class]
forCellReuseIdentifier:NSStringFromClass([WMHealthNewsVideoCell class])];
[self.tableView registerClass:[WMHealthNewsAdCell class]
forCellReuseIdentifier:NSStringFromClass([WMHealthNewsAdCell class])];
[self.tableView registerClass:[WMHealthNewsAudioCell class]
forCellReuseIdentifier:NSStringFromClass([WMHealthNewsAudioCell class])];
}
复制代码
解析数据:
// 解析数据
- (NSArray *)parseToModelArrayWithResponseObj:(id)responseObj {
NSMutableArray *newArray = [NSMutableArray new];
NSArray *items = responseObj[@"items"];
if (items.count > 0) {
[items enumerateObjectsUsingBlock:^(NSDictionary *contentDic, NSUInteger idx, BOOL * _Nonnull stop) {
WMNewsListModel *model = [[WMNewsListModel alloc] initWithDictionary:contentDic error:nil];
[newArray addObject:model];
}];
}
return [newArray copy];
}
复制代码
tableVIew代理
#pragma mark ----------- tableView Delegate -------------
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.dataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
WMNewsListModel *model = self.dataArray[indexPath.row];
if ([model.type isEqualToString:@"4"]) {
// 音频cell
WMHealthNewsAudioCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([WMHealthNewsAudioCell class]) forIndexPath:indexPath];
[cell setNewsModel:model];
return cell;
}else if ([model.type isEqualToString:@"5"]) {
// 视频cell
WMHealthNewsVideoCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([WMHealthNewsVideoCell class]) forIndexPath:indexPath];
[cell setNewsModel:model];
return cell;
}else if ([model.type isEqualToString:@"7"]) {
// 广告cell
WMHealthNewsAdCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([WMHealthNewsAdCell class]) forIndexPath:indexPath];
[cell setNewsModel:model];
return cell;
}else {
// 去除 音频.视频.广告 这三种样式cell外,其余的都按 showType 区分 小图.三图.大图
// showType: "1","小图一张", "2","大图一张", "3","小图三张"
if ([model.showType isEqualToString:@"2"]) {
// 大图样式
WMHealthNewsBigPicCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([WMHealthNewsBigPicCell class]) forIndexPath:indexPath];
[cell setNewsModel:model];
return cell;
}else if ([model.showType isEqualToString:@"3"]) {
// 三图样式
WMHealthNewsMorePicCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([WMHealthNewsMorePicCell class]) forIndexPath:indexPath];
[cell setNewsModel:model];
return cell;
}
// 小图 即标准图样式
WMHealthNewsNormalCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([WMHealthNewsNormalCell class]) forIndexPath:indexPath];
[cell setNewsModel:model];
return cell;
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
WMNewsListModel *model = self.dataArray[indexPath.row];
if (model.cellHeight > 0) {
return model.cellHeight;
}
model.cellHeight = [WMNewsListModel calutWholeCellHeightWithModel:model];
return model.cellHeight;
}
复制代码
这样就完成了资讯列表界面,效果如下: