前言
這幾天一直沒什么狀態,靜不下心來看其他新的東西。今天在上班的公交上突然想起上次有關網絡請求時的加載動畫,及加載后的加載失敗、數據為空等視圖在上篇筆記中(《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
類型,就說明是空數據的情況,設置其相應的行高(空數據界面的高度),并顯示其相應的視圖(空界面視圖)。
最終的效果: