造轮子之封装基类tableViewController

封装基类tableViewController来提升开发效率

列表类视图在开发中应该算是应用最多的了,为了减少不必要的代码量,避免重复造轮子,我在这里封装了一个基类的WMBaseTableViewController。

WMBaseTableViewController里面集合了刷新加载/空数据显示处理/网络状态异常显示处理,只需要传入接口url以及入参param 然后完成tableView代理就会生成一个完整的列表视图界面。可以让我们更关注于业务的逻辑而不用重复做冗余的代码工作。

1.WMBaseTableVC的封装

在.h里暴露出子类可能会用到的设置参数,包括:tableViewFrametableViewStyle含page的apiParam的blockapiManager的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;
}
复制代码

这样就完成了资讯列表界面,效果如下:

有数据时:

无数据时:

无网络时:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值