项目干货挖掘6——网络请求时的正在加载、加载失败、数据为空视图封装

前言

这几天一直没什么状态,静不下心来看其他新的东西。今天在上班的公交上突然想起上次有关网络请求时的加载动画,及加载后的加载失败、数据为空等视图在上篇笔记中(《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];
}

看看效果:

loadFailure.gif

更新 2017.1.17

前面我们说的加载失败界面和空数据界面的实现都是在控制器的view上添加子视图(盖一层视图)。这种实现方案在有些情况下是有问题的。
比如在某种情况下,当出现加载失败或空数据情况时不需要覆盖整个屏幕,此时虽然你也可以设置覆盖在上视图的frame,但它是不可以滑动的,而被覆盖在下的,露出来的tableView是可以滑动的。比如下图:该界面整体上是个tableView,上面菜单部分是tableViewtableHeadView,当是空数据情况时,空界面并不是充满整个屏幕的,而是要露出上面的菜单部分。若以覆盖一层view的方案实现的话,那当用户的手指落在菜单上滑动时,它竟然是可以继续滑动的。这种体验很生硬,能让用户明显得感觉到是有个东西覆盖在屏幕上最浮层遮住了后面的东西。

WechatIMG1.png

今天还遇到了一种情况是tableViewtableFooterView是个按钮,有操作事件。此时,当出现空数据情况时,tableView便一行都没有,tableFooterView便跑到tableView顶头了。此时给view上面覆盖一层显示空数据的视图的话,会遮盖住tableFooterView,用户看不到可点击的按钮了,这就是问题了。所以这种情况下上面所说的覆盖视图的方案也是不可行的。

总之,当加载失败界面或空数据界面不是覆盖整个屏幕时,直接在viewaddSubView:视图的方案要么出现挡住不该挡住的问题,要么即使没挡住什么东西,但是体验很生硬。此时,应当想想有没其他方案能比较完美的解决这个问题。

方案是有的:
我们可以以一个UITableViewCell作为空数据提示界面,这样便可以解决上面所说的问题。
正常有数据的情况下,tableView的数据model是放在一个数据源数组里tableViewData里的,且有其自定义样式的cell来展示数据。 当状态变为空数据情况时,我们可以将数据源清空,只放入一个表示空界面的字符串nullKey元素,同时,在tableView的代理方法里根据数据源元素的类型是model还是NSString来判断应该显示正常的cell还是表示空数据的NullDataCell。这样,当状态变为空数据状态,数据源清空并只放入nullKey后,刷新tableViewtableView就会显示表示空界面的NullDataCell

通过例子来说明。下图中是个tableViewcell的样式是默认的UITableViewCell类型,用于显示一行字符串。tableViewtableFootView是绿色部分,假设当出现空数据情况时不能被遮挡住,即空数据界面只能出现在白色部分。在导航栏的右边有个"null data"按钮,我们将其用于模拟出现空数据的情况。

屏幕快照 2017-01-17 23.03.24.png

看代码:
当点击右上角的“null data”按钮时,便执行下面的代码:将数据源数组清空并只添加一个表示空界面的字符串元素。然后刷新该tableView。此时,tableView的数据源只有一个,即tableView将只有一行,这一行便是空数据界面。

- (void)rightBtnClicked
{
    [_tableViewData removeAllObjects];
    [_tableViewData addObject:[NullDataCell nullKey]];
    [_tableView reloadData];
}

NullDataCellnullKey方法就是返回一个表示空界面的字符串:

+(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类型,就说明是空数据的情况,设置其相应的行高(空数据界面的高度),并显示其相应的视图(空界面视图)。

最终的效果:

屏幕快照 2017-01-17 23.12.59.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,066评论 4 62
  • 人当有了孩子后,会明白看透孩子背后的“”人生赢家”! 当孩子还在女人肚子里,该生的时候,有人问:给医生红包了没? ...
    岩娘阅读 164评论 0 0
  • “你的回话凌乱着 在这个时刻,我想起喷泉旁的白鸽 甜蜜散落了,情绪莫名的拉扯 我还爱你呢,而你断断续续唱着歌 假装...
    写情书的怪叔叔阅读 555评论 0 0
  • 1. CSS的全称是啥? cascading style sheets,中文翻译为层叠样式表。 2. CSS有几种...
    山门龙龙阅读 263评论 0 1