美文网首页iOS开发好文热门iOS开发技能
项目干货挖掘6——网络请求时的正在加载、加载失败、数据为空视图封

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

作者: Wang66 | 来源:发表于2016-07-04 16:56 被阅读1304次

前言

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

相关文章

网友评论

  • 经文纬武:很喜欢
  • mark666:自己好好考虑一下孰优孰略
    Wang66:@mark666 我是继承了基类的。在相应的控制器里只需要showTipsView:和hideTipsView方法就行了。http://www.jianshu.com/p/64cb60863f08
  • mark666:其实并不用你这么复杂的处理,最简单的办法写一个基类,加载状态的view添加上去,然后根据具体的状态然他显示不显示就可以了
    昵称glh:@mark666 能具体说一下怎么个继承吗
    mark666:@Wang66 懒加载创建就行,而且继承你这个基类的都拥有这个属性,你愿意每个界面写一大堆这样的垃圾代码?
    Wang66:@mark666 那其实在不需要显示这些视图的状态,视图是一直存在的。感觉这样不好。
  • 只因为趁年轻:楼主,我在加载失败的方法里 掉了removeNetworkTipView方法, 为什么失败页面不消失呢
    Wang66:@只因为趁年轻 你是说在loadedFailureHandle这个方法里创建了一个NetworkTipView的实例,然后调用它的移除方法removeNetworkTipView?
    只因为趁年轻:@Wang66 在加载失败里的回调方法, 我进行了网络判断, 网络正常的时候加载数据, 在加载数据前, 移除失败页面, 执行了- (void)removeNetworkTipView 方法, 但是失败的页面还浮在上面. 非常感谢.
    Wang66:@只因为趁年轻 能说详细点吗?我看看。 :blush:

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

本文链接:https://www.haomeiwen.com/subject/iabcjttx.html