前言
这几天一直没什么状态,静不下心来看其他新的东西。今天在上班的公交上突然想起上次有关网络请求时的加载动画,及加载后的加载失败、数据为空等视图在上篇笔记中(《UITextField一箩筐——输入长度限制、自定义placeholder、键盘遮挡问题》)没有写完整。在那篇笔记里,TipsView
只写了两种样式的正在加载动画的视图。既然今天别的东西看不进去,就先把这块的东西先完善一下吧。
思路分析
与正在加载动画是添加在keyWindow
上不同,加载失败和空数据的视图得添加在控制器的视图上,相当于在原本view
的上面覆盖了一层视图。因此基于上次写的代码,我们只需要在添加或创建这些视图的方法里,根据视图类型的参数来判断,若是要添加的是加载失败的视图,那我们就相应地创建其视图,然后添加到该控制器的view上
。
另外,加载失败不同于正在加载的地方是它是可点击的,有点击事件的。即点击之后重新加载,我们得把这个点击事件回调给控制器,在控制器里完成对网络数据的重新请求。上篇笔记中只写了正在加载视图,它是没有事件回调的。所以,我们得通过给方法添加一个block参数,用以回调点击事件。
关键点就是上面两点,当然其他地方也有微调。具体看下面的代码。
代码
** NetworkTipView.h **
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSInteger, TipType)
{
TipType_LoadingCenter = 0, // 只在屏幕中心的加载动画
TipType_loadingFull, // 覆盖整个屏幕的加载动画
TipType_loadedNull, // 数据为空
TipType_loadedFailure, // 加载失败
};
typedef void(^ClickBlock)(TipType tipType); // 点击事件回调(如一般加载失败后,点击重新加载)
@interface NetworkTipView : UIView
@property (nonatomic, assign)CGRect tipFrame;
@property (nonatomic, assign)CGSize tipImageSize;
@property (nonatomic, strong)UIColor *tipBgColor;
// 添加、创建tipView
- (void)addTipViewTarget:(id)target tipType:(TipType)type clickBlock:(ClickBlock)clickBlock;
// 移除tipView
- (void)removeNetworkTipView;
@end
** NetworkTipView.m **
#import "NetworkTipView.h"
#import "CommonViewController.h"
@interface NetworkTipView ()
{
UIView *_bgView;
CommonViewController *_vc;
ClickBlock _clickBlock;
}
@end
@implementation NetworkTipView
#pragma mark ---- 公共方法:提供给外部的功能方法
// 添加bgView到window上。
- (void)addTipViewTarget:(id)target tipType:(TipType)type clickBlock:(ClickBlock)clickBlock
{
if(_bgView){
[_bgView removeFromSuperview];
_bgView = nil;
}
// 确定frame
if([target isKindOfClass:[CommonViewController class]]){
_vc = (CommonViewController *)target;
// _tipFrame = _vc.contentView.frame;
}
_clickBlock = clickBlock;
_bgView = [[UIView alloc] initWithFrame:_vc.contentView.frame];
if(_tipBgColor){
_bgView.backgroundColor = _tipBgColor;
}else{
_bgView.backgroundColor = [UIColor whiteColor]; // 默认背景是白色
}
switch (type)
{
case TipType_loadingFull:
{
_bgView = [self createLoadingFullView]; // 创建bgView,即加载动画的核心视图
UIWindow *window = [UIApplication sharedApplication].keyWindow;
if (!window)
{
window = [[UIApplication sharedApplication].windows lastObject];
}
[window addSubview:_bgView];
}
break;
case TipType_LoadingCenter:
{
_bgView = [self createLoadingCenterView]; // 创建加载动画的核心视图
UIWindow *window = [UIApplication sharedApplication].keyWindow;
if (!window){
window = [[UIApplication sharedApplication].windows lastObject];
}
[window addSubview:_bgView];
}
break;
case TipType_loadedNull:
case TipType_loadedFailure:
{
_bgView = [self createLoadedNullOrFailureView:type];
[_vc.view addSubview:_bgView];
}
break;
}
}
// 移除tipView
- (void)removeNetworkTipView
{
if(_bgView)
{
if([_bgView.superview isKindOfClass:[UIWindow class]]){
[_bgView removeFromSuperview];
_bgView = nil;
}
}
}
#pragma mark ---- 内部功能方法
// 创建加载动画核心视图
- (UIView *)createLoadingFullView
{
UIImage *image = [UIImage imageNamed:@"Network_loading1"];
if(_tipImageSize.width == 0){
_tipImageSize = CGSizeMake(image.size.width/2.f, image.size.height/2.f);
}
UIImageView *animationView = [[UIImageView alloc] initWithFrame:CGRectMake((_bgView.frame.size.width-_tipImageSize.width)/2.f, (_bgView.frame.size.height-_tipImageSize.height)/2.f, _tipImageSize.width, _tipImageSize.height)];
animationView.image = image;
animationView.backgroundColor = [UIColor whiteColor];
animationView.animationImages = [NSArray arrayWithObjects:
image,
[UIImage imageNamed:@"Network_loading2.png"],
[UIImage imageNamed:@"Network_loading3.png"],
[UIImage imageNamed:@"Network_loading4.png"],
[UIImage imageNamed:@"Network_loading5.png"],
[UIImage imageNamed:@"Network_loading6.png"],
[UIImage imageNamed:@"Network_loading7.png"],
[UIImage imageNamed:@"Network_loading8.png"],
[UIImage imageNamed:@"Network_loading9.png"],
[UIImage imageNamed:@"Network_loading10.png"],
[UIImage imageNamed:@"Network_loading11.png"],
[UIImage imageNamed:@"Network_loading12.png"],
[UIImage imageNamed:@"Network_loading13.png"],
[UIImage imageNamed:@"Network_loading14.png"],
[UIImage imageNamed:@"Network_loading15.png"],
[UIImage imageNamed:@"Network_loading16.png"],
[UIImage imageNamed:@"Network_loading17.png"],
[UIImage imageNamed:@"Network_loading18.png"],
nil];
[animationView setAnimationDuration:1.0f];
[animationView setAnimationRepeatCount:-1];
[animationView startAnimating];
[_bgView addSubview:animationView];
return _bgView;
}
- (UIView *)createLoadingCenterView
{
CGFloat black_w = 60.f;
UIView *blackView = [[UIView alloc] initWithFrame:CGRectMake((_bgView.frame.size.width-black_w)/2.f, (_bgView.frame.size.height-black_w)/2.f, black_w, black_w)];
blackView.backgroundColor = [UIColor blackColor];
blackView.alpha = 0.6f;
blackView.layer.masksToBounds = YES;
blackView.layer.cornerRadius = 5.f;
[_bgView addSubview:blackView];
UIActivityIndicatorView *activityV = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(blackView.frame.size.width/4.f, blackView.frame.size.height/4.f, blackView.frame.size.width/2.f, blackView.frame.size.height/2.f)];
activityV.center = CGPointMake(blackView.bounds.size.width/2.f, blackView.bounds.size.height/2.f);
activityV.backgroundColor = [UIColor clearColor];
[activityV startAnimating];
[blackView addSubview:activityV];
[_bgView addSubview:blackView];
return _bgView;
}
// 创建加载失败和空数据的视图。因写的是demo,就写在一个方法里了,文字不同而已。
- (UIView *)createLoadedNullOrFailureView:(TipType)tipType
{
UIView *whiteView = [[UIView alloc] init];
if(_tipFrame.size.width!=0){
whiteView.frame = _tipFrame;
}else{
whiteView.frame = CGRectMake(0, 0, _bgView.frame.size.width, _bgView.frame.size.height);
}
whiteView.backgroundColor = [UIColor whiteColor];
[_bgView addSubview:whiteView];
UILabel *nullLab = [[UILabel alloc] initWithFrame:CGRectMake((whiteView.frame.size.width-200.f)/2.f, (whiteView.frame.size.height-80.f)/2.f, 200.f, 80.f)];
nullLab.textAlignment = NSTextAlignmentCenter;
nullLab.textColor = [UIColor lightGrayColor];
nullLab.font = [UIFont systemFontOfSize:12];
if(tipType == TipType_loadedNull){
nullLab.text = @"没有内容";
}else if(tipType == TipType_loadedFailure){
nullLab.text = @"加载失败,点击重新加载";
nullLab.userInteractionEnabled = YES;
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(labGestureHandle:)];
[nullLab addGestureRecognizer:tapGesture];
}
[whiteView addSubview:nullLab];
return _bgView;
}
- (void)labGestureHandle:(UITapGestureRecognizer *)tapGesture
{
_clickBlock(TipType_loadedFailure);
}
@end
** CommonViewController.m **
只列出相关代码:在添加这些视图的方法里加上了block回调,用于处理回调事件。这里的情况是该block的参数是TipType
类型,表示当前所创建的视图类型,若为加载失败视图的事件点击回调,那我们得重新进行网络数据请求呀。
// 显示、出现tipView
- (void)showNetworkTipsView:(TipType)tipType
{
if(!_networkTipView){
_networkTipView = [[NetworkTipView alloc] init];
}
__block id weakSelf = self;
[_networkTipView addTipViewTarget:self tipType:tipType clickBlock:^(TipType tipType) {
if(tipType == TipType_loadedFailure){
[weakSelf loadedFailureHandle]; // 目前只有数据加载的视图是有点击事件回调的
}
}];
}
// 网络请求失败 点击重试
-(void)loadedFailureHandle
{
// 在子类中重写
}
loadedFailureHandle
这是在基类CommonViewController
中定义,我们在相关类中重写它,实现具体的网络请求等。比如在控制器HomeViewController
中重写了它,在里面再次请求了学生列表的网络数据。
- (void)loadedFailureHandle
{
[self startNetworkRequestAction:@selector(requestStudentList) tipType:TipType_LoadingCenter];
}
看看效果:
更新 2017.1.17
前面我们说的加载失败界面和空数据界面的实现都是在控制器的view
上添加子视图(盖一层视图)。这种实现方案在有些情况下是有问题的。
比如在某种情况下,当出现加载失败或空数据情况时不需要覆盖整个屏幕,此时虽然你也可以设置覆盖在上视图的frame
,但它是不可以滑动的,而被覆盖在下的,露出来的tableView
是可以滑动的。比如下图:该界面整体上是个tableView
,上面菜单部分是tableView
的tableHeadView
,当是空数据情况时,空界面并不是充满整个屏幕的,而是要露出上面的菜单部分。若以覆盖一层view
的方案实现的话,那当用户的手指落在菜单上滑动时,它竟然是可以继续滑动的。这种体验很生硬,能让用户明显得感觉到是有个东西覆盖在屏幕上最浮层遮住了后面的东西。
今天还遇到了一种情况是tableView
的tableFooterView
是个按钮,有操作事件。此时,当出现空数据情况时,tableView
便一行都没有,tableFooterView
便跑到tableView
顶头了。此时给view
上面覆盖一层显示空数据的视图的话,会遮盖住tableFooterView
,用户看不到可点击的按钮了,这就是问题了。所以这种情况下上面所说的覆盖视图的方案也是不可行的。
总之,当加载失败界面或空数据界面不是覆盖整个屏幕时,直接在view
上addSubView:
视图的方案要么出现挡住不该挡住的问题,要么即使没挡住什么东西,但是体验很生硬。此时,应当想想有没其他方案能比较完美的解决这个问题。
方案是有的:
我们可以以一个UITableViewCell
作为空数据提示界面,这样便可以解决上面所说的问题。
正常有数据的情况下,tableView
的数据model
是放在一个数据源数组里tableViewData
里的,且有其自定义样式的cell
来展示数据。 当状态变为空数据情况时,我们可以将数据源清空,只放入一个表示空界面的字符串nullKey
元素,同时,在tableView
的代理方法里根据数据源元素的类型是model
还是NSString
来判断应该显示正常的cell
还是表示空数据的NullDataCell
。这样,当状态变为空数据状态,数据源清空并只放入nullKey
后,刷新tableView
,tableView
就会显示表示空界面的NullDataCell
。
通过例子来说明。下图中是个tableView
,cell
的样式是默认的UITableViewCell
类型,用于显示一行字符串。tableView
的tableFootView
是绿色部分,假设当出现空数据情况时不能被遮挡住,即空数据界面只能出现在白色部分。在导航栏的右边有个"null data"按钮,我们将其用于模拟出现空数据的情况。
看代码:
当点击右上角的“null data”按钮时,便执行下面的代码:将数据源数组清空并只添加一个表示空界面的字符串元素。然后刷新该tableView
。此时,tableView
的数据源只有一个,即tableView
将只有一行,这一行便是空数据界面。
- (void)rightBtnClicked
{
[_tableViewData removeAllObjects];
[_tableViewData addObject:[NullDataCell nullKey]];
[_tableView reloadData];
}
NullDataCell
的nullKey
方法就是返回一个表示空界面的字符串:
+(NSString*)nullKey
{
return @"NullData";
}
此时,我们调换了数据源,并刷新了tableView
想让其重新加载一遍,是因为我们需要在tableView
的代理方法里同样调换cell
,将UITableViewCell
调换为NullDataCell
。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
id obj = _tableViewData[indexPath.row];
if([obj isKindOfClass:[NSString class]])
{
if([obj isEqualToString:[NullDataCell nullKey]]){
static NSString *nullCellId = @"nullCellId";
NullDataCell *cell = [tableView dequeueReusableCellWithIdentifier:nullCellId];
if(cell==nil){
cell = [[NullDataCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nullCellId];
}
return cell;
}
}
else
{
CellModel *model = (CellModel *)obj;
static NSString *cellId = @"cellId";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];
if(cell==nil){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellId];
}
cell.textLabel.text = model.title;
return cell;
}
return nil;
}
在tableView
的代理方法里,判断数据源数组的元素是什么类型,如果是CellModel
类型,就说明是正常情况,设置其相应的行高,并显示其相应的视图;如果数组元素是NSString
类型,就说明是空数据的情况,设置其相应的行高(空数据界面的高度),并显示其相应的视图(空界面视图)。
最终的效果: