iOS小技能: 网络加载中处理、接口暂无数据处理、全局监听用户点击事件

491 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第20天,点击查看活动详情

引言

在日常开发中经常涉及数据列表的查询,处理服务侧无数据返回的情况或者网络异常的手段是iOS必备小技能。

如果是iOS新手,可以先看第三章节的预备知识。

I 网络加载中处理

本文以SVProgressHUD框架为例子进行讲解

1.1 UIWindowLevel

UIWindowLevel级别的高低顺序从小到大为Normal < StatusBar < Alert,Level高的将排在所有Level比他低的层级的前面。

    UIKIT_EXTERN const UIWindowLevel UIWindowLevelNormal;
    UIKIT_EXTERN const UIWindowLevel UIWindowLevelAlert;
    UIKIT_EXTERN const UIWindowLevel UIWindowLevelStatusBar API_UNAVAILABLE(tvos);

SVProgressHUD 相关方法

    [SVProgressHUD setMaxSupportedWindowLevel:UIWindowLevelStatusBar];

1.2 图层样式

默认图层样式显示的时候,允许用户交互适合大部分场景。但是像注册、登录、切换账号的场景建议使用不允许交互样式。

请求包含这些关键字的API时,建议不允许交互 :CreateAddUpdateDeleteBindSubmitSaveLoginChangeSetEditModifyCancelSendCheckAudit

typedef NS_ENUM(NSUInteger, SVProgressHUDMaskType) {
    SVProgressHUDMaskTypeNone = 1,  // default mask type, allow user interactions while HUD is displayed
    SVProgressHUDMaskTypeClear,     // don't allow user interactions with background objects
    SVProgressHUDMaskTypeBlack,     // don't allow user interactions with background objects and dim the UI in the back of the HUD (as seen in iOS 7 and above)
    SVProgressHUDMaskTypeGradient,  // don't allow user interactions with background objects and dim the UI with a a-la UIAlertView background gradient (as seen in iOS 6)
    SVProgressHUDMaskTypeCustom     // don't allow user interactions with background objects and dim the UI in the back of the HUD with a custom color
};

  1. 加载框展示自定义图片:setInfoImage:
// 设置加载信息
    NSString *path = [[NSBundle mainBundle] pathForResource:@"加载" ofType:@"gif"];
    NSData *data = [NSData dataWithContentsOfFile:path];
    UIImage *image = [UIImage sd_imageWithGIFData:data];
    [SVProgressHUD setInfoImage:image];
    [SVProgressHUD setImageViewSize:CGSizeMake(60, 60)];
    [SVProgressHUD showInfoWithStatus:@""];

  1. 使用默认样式展示相关文字信息:showWithStatus
    [SVProgressHUD setBackgroundColor: [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.30]];
    [SVProgressHUD setForegroundColor: UIColor.whiteColor];
    [SVProgressHUD setMaximumDismissTimeInterval:0.5];//
    [SVProgressHUD showWithStatus:@"加载中..."];

1.3 灵活控制是否允许交互

思路: 通过请求方法入口进行控制是否允许交互 实现步骤:

  1. 配置不允许交互的URL
  2. 展示加载框时,判断URL是否允许交互,即在调用showLoadingDataGif时判断
//
//  ERPLoading.m
//  retail
//
//  Created by mac on 2022/8/10.
//  Copyright © 2022 QCT. All rights reserved.
//

#import "ERPLoading.h"

@interface ERPLoading ()

/**
 不允许交互的URL
 */
@property (strong, nonatomic) NSMutableArray *urls4no_allow_user_interactions;

@end

@implementation ERPLoading
HSSingletonM(ERPLoading);
//配置不允许交互的URL
- (NSMutableArray *)urls4no_allow_user_interactions{
    
    if(_urls4no_allow_user_interactions == nil){
        
        _urls4no_allow_user_interactions = [NSMutableArray array];
        //登录 /api/xxx/xxx
        [_urls4no_allow_user_interactions addObject:URL_Login];
        //切换门店
        [_urls4no_allow_user_interactions addObject:k_API_Users_ChangeStore];
        
    }
    
    return _urls4no_allow_user_interactions;
}

/**
 判断是否允许交互:
 URL参数是绝对路径,urls4no_allow_user_interactions数组中的元素是相对路径 因此先对URL去除域名部分,只留相对路径再进行内容的比较。

 */
+(BOOL)isAllow_user_interactions:(NSString*)url{
    NSString *separate =@"/api/";// 由于API格式统一是 /api/xxx/xxx,因此可以简单的采用字符串分割即可。
    url = [url componentsSeparatedByString:separate].lastObject;
    url= FMSTR(@"%@%@",separate,url);// 这里可以拼接上分隔符之后直接调用数组的containsObject方法进行比较,也可以采用谓词技术进行匹配https://blog.csdn.net/z929118967/article/details/74066170
    NSLog(@"relativeString:%@",url);
    
    if([[ERPLoading shareERPLoading].urls4no_allow_user_interactions containsObject:url])
        
    {
        [self no_allow_user_interactions];
        return NO;
    }
    [self allow_user_interactions];
    return YES;
}

//allow user interactions
+(void)allow_user_interactions{
    [SVProgressHUD setDefaultMaskType:SVProgressHUDMaskTypeNone];
}
//don't allow user interactions
+(void)no_allow_user_interactions{
    [SVProgressHUD setDefaultMaskType:SVProgressHUDMaskTypeCustom];
}
// 加载的gif
+ (void)showLoadingDataGifWith:(NSString*)url{
    //1. 判断是否允许交互
    [self isAllow_user_interactions:url];
// 2. 设置加载信息
    NSString *path = [[NSBundle mainBundle] pathForResource:@"加载" ofType:@"gif"];
    NSData *data = [NSData dataWithContentsOfFile:path];
    UIImage *image = [UIImage sd_imageWithGIFData:data];
    [SVProgressHUD setInfoImage:image];
    [SVProgressHUD setImageViewSize:CGSizeMake(60, 60)];
    [SVProgressHUD setBackgroundColor:[UIColor clearColor]];
//    [SVProgressHUD setBackgroundColor: [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.30]];
//    [SVProgressHUD setForegroundColor: UIColor.whiteColor];

    // 设置最小显示时长。default is 0 seconds
//    [SVProgressHUD setMinimumDismissTimeInterval:0.4];
    [SVProgressHUD setBackgroundLayerColor:STModalWindowDefaultBackgroundColor];
    [SVProgressHUD showInfoWithStatus:@""];
}
// 隐藏gif
+ (void)hiddenLoadingDataGif{
    [SVProgressHUD dismiss];
}
@end

1.4 点击空白处隐藏提示框

实现思路: 自定义应用对象UIApplication来全局监听用户点击事件

实现步骤:

  1. 自定义应用,并重写sendEvent:
@implementation ERPApplication

- (void)sendEvent:(UIEvent *)event
{
    //如果调用了[super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者。
    [super sendEvent:event];//这里一定不能漏掉
    ///点击空白处隐藏提示框:
    [ERPLoading setupTouch4hiddenLoadingDataGif:event];
    
}


  1. 在main方法创建应用对象的传递自定义的应用对象类型。
 int UIApplicationMain(int argc, char * _Nullable *argv, NSString *principalClassName, NSString *delegateClassName);
// Creates the application object and the application delegate and sets up the event cycle.
  1. 点击空白处隐藏提示框: 监听点击事件,如果是允许交互的样式SVProgressHUDMaskTypeNone则调用dismiss。
+ (void)setupTouch4hiddenLoadingDataGif:(UIEvent *)event{
    

    //当用户用一根手指触摸屏幕时,会创建一个与手指相关联的UITouch对象;一根手指对应一个UItouch对象。
    //UItouch保存着跟手指相关的信息,比如触摸的位置、时间、阶段。
    //根据touches中UITouch的个数可以判断出是单点触摸还是多点触
    NSSet *allTouches = [event allTouches];

    if ([allTouches count] <= 0)

    {
        return ;
        
    }
    
    UITouchPhase phase = ((UITouch *)[allTouches anyObject]).phase;

    if (phase != UITouchPhaseBegan){
        
        return ;
    }

    
//点击空白处隐藏提示框:点击空白处隐藏提示框: 监听点击事件,如果是允许交互的样式SVProgressHUDMaskTypeNone则调用dismiss。
    SVProgressHUD *sharedView =[self sharedView];
    
    NSLog(@"%@: defaultMaskType: %lu send event",UIApplication.sharedApplication,(unsigned long)sharedView.defaultMaskType);
    
    
    if(sharedView.defaultMaskType == SVProgressHUDMaskTypeNone ){
        [self hiddenLoadingDataGif];
    }


}

+(SVProgressHUD*)sharedView{
    
    return  [SVProgressHUD performSelector:@selector(sharedView)];
    
    
}

II 暂无数据处理

网络请求失败,业务逻辑错误,返回数据为空都是需要处理界面的显示,推荐使用暂无数据进行提示。

在这里插入图片描述

2.1 用法

        if (weakSelf.viewModel.listDataArray.count == 0) {
            [weakSelf.viewModel.ShowNoviewSubject sendNext:QCTLocal(CRM_nodata_Info)];
            
        }else{
            [weakSelf.viewModel.hidenNoviewSubject sendNext:nil];
            
        }

2.2 核心实现

V层初始化暂无数据视图:将视图添加到tableView,这样可以不影响下拉刷新和上拉加载

- (CRMNoDatatView *)NoView{
    
    if (nil == _NoView) {
        
        CRMNoDatatView *tmpView = [[CRMNoDatatView alloc]init];
        
        _NoView = tmpView;
        [self.tableView addSubview:_NoView];
        
        __weak __typeof__(self) weakSelf = self;
        
        
        [_NoView mas_makeConstraints:^(MASConstraintMaker *make) {
            
            
            make.centerY.equalTo(weakSelf.tableView.mas_centerY).offset(kAdjustRatio(k_noteViewH));
            make.width.equalTo(weakSelf);
            
            
            
            make.left.right.bottom.equalTo(weakSelf.tableView);//tableView


            
        }];
        
        
        
    }
    return _NoView;
}



- (void)ShowNoview:(NSString *)title img:(NSString*)imgName
{

    
    self.NoView.title = title;
    
    self.NoView.imgName = imgName;
    
    [self.tableView bringSubviewToFront:self.NoView];
    
}

V层监听C层的事件

    [self.viewModel.hidenNoviewSubject subscribeNext:^(id  _Nullable x) {
        weakSelf.NoView.hidden = YES;
    }];
    
    [self.viewModel.ShowNoviewSubject subscribeNext:^(id  _Nullable x) {
        weakSelf.NoView.hidden = NO;
        
        [weakSelf ShowNoview:x img:@"img_kongbai_zanwu"];

        
        
    }];

暂无数据视图的实现

// 显示暂无数据图片
- (UIImageView *)imageV{
    if (nil == _imageV) {
        UIImageView *tmpView = [[UIImageView alloc]init];
        _imageV = tmpView;
        
        _imageV.contentMode = UIViewContentModeScaleAspectFit;

        _imageV.image = [UIImage imageNamed:@"icon_wushuju"];

        [self addSubview:_imageV];
        __weak __typeof__(self) weakSelf = self;

        
        [_imageV mas_makeConstraints:^(MASConstraintMaker *make) {
            make.centerX.equalTo(weakSelf);
            make.centerY.equalTo(weakSelf).offset(-kAdjustRatio(35));

            make.left.equalTo(weakSelf).offset(kAdjustRatio(33));
            
            make.right.equalTo(weakSelf).offset(kAdjustRatio(-33));
            
            

        }];
    }
    return _imageV;
}


//显示暂无数据文本
- (UILabel *)label{
    if (nil == _label) {
        UILabel *tmpView = [[UILabel alloc]init];
        _label = tmpView;
        [self addSubview:_label];
        __weak __typeof__(self) weakSelf = self;

        
        [_label mas_makeConstraints:^(MASConstraintMaker *make) {
            
            make.centerX.equalTo(weakSelf);
            make.top.equalTo(weakSelf.imageV.mas_bottom).offset(kAdjustRatio(22));
            
        
        _label.textAlignment = NSTextAlignmentCenter;

        
        _label.font = kPingFangFont(15);
        _label.textColor = rgb(51,51,51);

    }
    return _label;
}




// 更新图片数据
-(void)setImgName:(NSString *)imgName{
    _imgName = imgName;
    
    
    if (imgName.length<=0) {
        return;
    }
    [self.imageV setImage:[UIImage imageNamed:imgName]];
    
    
        self.reloadbtnView.hidden = !self.isShowReloadBtn;
//    }
}

- (void)setTitle:(NSString *)title{
    _title = title;
    
    self.label.text = title;
}

III 预备储备:Target-Action设计模式的运用

Target-Action指当某个事件发生的时候,调用特定对象的特定方法。 blog.csdn.net/z929118967/…

3.1 响应者链

在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接收并处理事件,我们称之为“响应者对象”。 UIApplication、UIViewController、UIView都继承于UIResponder。

UIResponder内部提供了以下方法来处理事件

//一根或者多根手指开始触摸view,系统会自动调用view的下面方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法)
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//一根或者多根手指离开view,系统会自动调用view的下面方法
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法[可选]
- (void)touchesCancelled:(nullable NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;


响应者链的事件传递过程 在这里插入图片描述

Responder Chain(响应者链)【上篇】:kunnan.blog.csdn.net/article/det…

Responder Chain(响应者链)【下篇】:kunnan.blog.csdn.net/article/det…

3.2 限制按钮的点击频率

在项目开发中,会对数据库数据进行更新操作的接口请求,不仅服务器侧需要控制请求频率以及保证数据的唯一性和一致性,app侧也需要进行限制来避免产生垃圾数据。

常用的方案有:

  1. 限制按钮的点击频率: 针对注册类接口的时间间隔timeInterval可设置长些,推荐0.5s
  2. 新增标志对单个接口进行请求频率的控制

———————————————— 版权声明:本文为CSDN博主「iOS逆向」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请 附上原文出处链接及本声明。

原文链接:blog.csdn.net/z929118967/…

3.2 右滑返回

原理:利用系统的返回手势interactivePopGestureRecognizer进行实现

使用场景: 返回按钮有点小,不好触发返回时,可借助右滑返回来提升用户体验

blog.csdn.net/z929118967/…

3.3 应用程序初识

blog.csdn.net/z929118967/… 每一个应用都有自己的UIApplication对象,而且是单例的;[UIApplication sharedApplication]

  1. 一个iOS程序启动后创建的第一个对象,就是UIApplication对象
  2. 利用UIApplication对象可以进行一些应用级的操作: 状态栏管理、事件处理
int main(int argc, char * argv[]) {
    @autoreleasepool {
        //principalClassName:        The name of the UIApplication class or subclass. If you specify nil, UIApplication is assumed.
//ERPApplication
        
        return UIApplicationMain(argc, argv, @"ERPApplication", NSStringFromClass([AppDelegate class]));
        
    }
}

  1. 在app受到干挠(来电、锁屏)时,会产生些系统事件,让delegate处理这些系统事件
  • 应用程序的生命周期(程序启动、关闭): 处理苹果服务器推送的消息。
  • 系统事件(来电)
  • 内存警告

see also

更多内容请关注 #小程序:iOS逆向,只为你呈现有价值的信息,专注于移动端技术研究领域。