读MBProgressHUD

日常的开发中,咱们一般会在处理一些耗时任务的时候,在界面上显示一个加载的标识(或者一个进度条),这样会让用户知道app是在作事情,而不是像卡死了的一动不动的中止在那里。git

MBProgressHUD:做为iOS开发的开发者,即使没用过,也应该听过,做为加载动画的第三方,我本身对这个库的感受就是简单、好用。以前也看过MBProgressHUD的源码,当时看了,以为做者的实现很简单,自定义一个view,有一个展现和收起的方法。就直接封装了一层,开始用了。如今回头来,再看,除了看做者如何实现以外,我还有了其它的收获,尤为是做者代码的布局,清晰易懂,看到做者的代码后,笔者本身直观的感觉就是很畅快、愿意看下去。github

涉及到的类

首先咱们来看下这个库涉及到的类:bash

MBProgressHUD:核心类,咱们在外部直接调用这个类,生成这个类的一个实例,而后加到咱们想要加到的viewwindow上。app

MBBackgroundView:根据类名也能够看出来,这个类的做用是做为背景视图的。做者自定义了这个类,给视图上加了一个UIVisualEffectView,显示出来虚化的效果。ide

MBRoundProgressView:自定义的圆形加载视图。oop

MBBarProgressView:自定义的条形加载视图。布局

MBProgressHUDRoundedButton:这是一个私有的类,没有对外公开的继承于UIButton的一个类。做者的处理也比较简单:重写了intrinsicContentSize方法,将button的固有大小增大了宽度;加了圆角和边框。post


咱们主要来看下MBProgressHUD的实现:单元测试

初始化:

经过做者的 #pragma mark能够很清晰的看到做者的代码条理,有指定初始化方法 - (instancetype)initWithFrame:(CGRect)frame和便利初始化方法 - (instancetype)initWithView:(UIView *)view供外部调用。初始化方法中调用了 commonInit来作相关配置和布局。

commonInit方法中调用了registerForNotifications方法在通知中心添加了观察者,同时直接在dealloc方法中去掉。这里很清晰地成对的对通知进行添加和去除,从而避免忘了去除观察者。学习

UI布局

从上图中的方法声明中看到: setupViews方法是添加一些背景视图、label等一些固定的不会变化的视图;而 updateIndicators是单独来布局指示器视图的,也是该库功能的核心体现视图。对外提供了设置hud的 mode的接口,因此,这个指示器视图就会有不少种状况,做者单独将其拿出来。做者经过约束进行视图布局。

接下来咱们来看下hud最终呈现出来的视图层次:

- (void)setupViews {//中间省略了一些代码
    UIColor *defaultColor = self.contentColor;
    MBBackgroundView *backgroundView = [[MBBackgroundView alloc] initWithFrame:self.bounds];
    、、、、、、
    [self addSubview:backgroundView];
    _backgroundView = backgroundView;
    MBBackgroundView *bezelView = [MBBackgroundView new];
   、、、、、、
    [self addSubview:bezelView];
    _bezelView = bezelView;
    [self updateBezelMotionEffects];
    UILabel *label = [UILabel new];
、、、、、、
    _label = label;
    UILabel *detailsLabel = [UILabel new];
、、、、、、
    _detailsLabel = detailsLabel;
    UIButton *button = [MBProgressHUDRoundedButton buttonWithType:UIButtonTypeCustom];
 、、、、、、
    _button = button;
    for (UIView *view in @[label, detailsLabel, button]) {
        view.translatesAutoresizingMaskIntoConstraints = NO;
        [view setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisHorizontal];
        [view setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisVertical];
        [bezelView addSubview:view];
    }

//这两个view是默认隐藏的,用来作约束用。
    UIView *topSpacer = [UIView new];
    topSpacer.translatesAutoresizingMaskIntoConstraints = NO;
    topSpacer.hidden = YES;
    [bezelView addSubview:topSpacer];
    _topSpacer = topSpacer;

    UIView *bottomSpacer = [UIView new];
    bottomSpacer.translatesAutoresizingMaskIntoConstraints = NO;
    bottomSpacer.hidden = YES;
    [bezelView addSubview:bottomSpacer];
    _bottomSpacer = bottomSpacer;
}
复制代码

经过层级图配合代码来看MBProgressHUD的UI布局:为了方便查看:笔者将不一样的view设置了不一样的颜色。

  • 红色:层级图中的最底层的红色view是MBProgressHUD的实例hud。
  • 黄色:层级中黄色的view是MBBackgroundView的实例,做为hud的背景视图backgroundView
  • 绿色的view也是MBBackgroundView类的实例bezelView,做为真正显示loading图的容器view。该视图上布局labeldetailLabelindicator、和button。其中indicator做为私有属性,根据设置hud的mode的不一样,从而设置不一样的indicator,所以做者设置属性的时候将indicator的属性类型设置为UIView。

在这里要提醒注意的是:若是要设置mode为MBProgressHUDModeCustomView,就是你不想用第三方提供的一些指示器视图,想本身自定的话,你自定义的view必须实现intrinsicContentSize方法,得到一个合适的大小。由于做者的布局是经过约束,不是利用frame的,做者在方法的注释里也说明了,该自定义视图须要实现intrinsicContentSize固有大小的方法来得到一个合适的尺寸。系统中的UILabelUIButtonUIImageView都默认已经实现了intrinsicContentSize这个方法,若是你的自定义view就直接是继承于UIView类的话,那么须要实现这个intrinsicContentSize方法。

show&hide功能实现

做者设置了四个定时器来实现展现和隐藏的功能。

@property (nonatomic, weak) NSTimer *graceTimer;
@property (nonatomic, weak) NSTimer *minShowTimer;
@property (nonatomic, weak) NSTimer *hideDelayTimer;
@property (nonatomic, weak) CADisplayLink *progressObjectDisplayLink;
复制代码

咱们就从这四个定时器的使用来查看做者的实现:

  • graceTimer:和graceTimer相关联的一个属性是graceTime:宽限时间。这个属性的用途是:当任务执行的很快的时候,就不须要弹出来hud。至关于给你的任务设置一个最小的耗时时间,好比:0.5;就是当你的任务耗时超过0.5秒以上时,才会触发hud的弹出展现。
- (void)showAnimated:(BOOL)animated {
    MBMainThreadAssert();
    [self.minShowTimer invalidate];
    self.useAnimation = animated;
    self.finished = NO;
    // If the grace time is set, postpone the HUD display
    //设置了graceTime后,hud的弹出展现将会经过self.graceTimer这个定时器延时触发。
    if (self.graceTime > 0.0) {
        NSTimer *timer = [NSTimer timerWithTimeInterval:self.graceTime target:self selector:@selector(handleGraceTimer:) userInfo:nil repeats:NO];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        self.graceTimer = timer;
    } 
    // ... otherwise show the HUD immediately
    else {
        [self showUsingAnimation:self.useAnimation];
    }
}
复制代码
  • minShowTimer:最少展现时间定时器,相关联的属性是minShowTime。避免hud刚展现就给隐藏了。
- (void)hideAnimated:(BOOL)animated {
    MBMainThreadAssert();//hud的show和hide都必须在主线程中操做,做者加了断言判断。
    [self.graceTimer invalidate];//若是手动调用了hide方法,此时若是设置的gracetiem还没到,尚未触发show方法的话,就直接不须要触发show了,直接将self.graceTimer弃用。
    self.useAnimation = animated;
    self.finished = YES;
    // If the minShow time is set, calculate how long the HUD was shown,
    // and postpone the hiding operation if necessary
    if (self.minShowTime > 0.0 && self.showStarted) {//避免瞬间的展现和收起,在这里若是设置了最少展现时间的话,就在这里计算下还需展现多长时间来让启动self.minShowTimer让hud收起。
        NSTimeInterval interv = [[NSDate date] timeIntervalSinceDate:self.showStarted];
        if (interv < self.minShowTime) {
            NSTimer *timer = [NSTimer timerWithTimeInterval:(self.minShowTime - interv) target:self selector:@selector(handleMinShowTimer:) userInfo:nil repeats:NO];
            [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
            self.minShowTimer = timer;
            return;
        } 
    }
    // ... otherwise hide the HUD immediately
    [self hideUsingAnimation:self.useAnimation];
}

复制代码
  • hideDelayTimer:延时隐藏定时器,这个定时器主要是为了- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay接口而设置。
- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay {
    // Cancel any scheduled hideAnimated:afterDelay: calls
    [self.hideDelayTimer invalidate];
    NSTimer *timer = [NSTimer timerWithTimeInterval:delay target:self selector:@selector(handleHideTimer:) userInfo:@(animated) repeats:NO];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    self.hideDelayTimer = timer;
}
复制代码

若是重复调用了这个方法,会让以前的hideDelayTimer定时器弃用,再以新的定时器来开始计时延时隐藏。

  • progressObjectDisplayLink:这个定时器是根据屏幕的刷新的帧率来触发updateProgressFromProgressObject刷新进度条的方法。而这个定时器也只有在设置了progressObject属性后才会建立。经过这个progressObject属性将进度信息反馈到hud,从而不断更新进度条。

综上:经过以上四个定时器的操做能够看出hud弹出和收起的逻辑处理。最终是经过根据bezelView的形变将hud显示出来。能够经过completionBlock或者设置MBProgressHUDDelegate的代理来进行hud隐藏后的回调操做。

- (void)done {
    [self setNSProgressDisplayLinkEnabled:NO];//隐藏后,将progressObjectDisplayLink弃用失效
    if (self.hasFinished) {
        self.alpha = 0.0f;
        if (self.removeFromSuperViewOnHide) {
            [self removeFromSuperview];//
        }
    }
    MBProgressHUDCompletionBlock completionBlock = self.completionBlock;
    //触发回调block
    if (completionBlock) {
        completionBlock();
    }
    //触发代理
    id<MBProgressHUDDelegate> delegate = self.delegate;
    if ([delegate respondsToSelector:@selector(hudWasHidden:)]) {
        [delegate performSelector:@selector(hudWasHidden:) withObject:self];
    }
}
复制代码

单元测试

从上述的hud的实现,咱们能够看出做者将初始化、布局、约束、弹出和隐藏及相关属性的设置都很细致的分别拆分开来做为单独的方法。这样就很方便将每个方法当作use case来进行单元测试。

经过#pragma mark查看:

做者覆盖了几乎全部方法的测试;并且做者在测试文件里也将测试代码布局的清晰有条理。

- (void)testInitializers {
    XCTAssertNotNil([[MBProgressHUD alloc] initWithView:[UIView new]]);
    UIView *nilView = nil;
    XCTAssertThrows([[MBProgressHUD alloc] initWithView:nilView]);
    XCTAssertNotNil([[MBProgressHUD alloc] initWithFrame:CGRectZero]);
    NSKeyedUnarchiver *dummyUnarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:[NSData data]];
    XCTAssertNotNil([[MBProgressHUD alloc] initWithCoder:dummyUnarchiver]);
}
复制代码

咱们来看下测试初始化方法,做者测试了他给出的全部的指定初始化方法,正常和异常的都有测试。

最后,经过阅读MBProgressHUD的源码,笔者认为最大的收获是做者的代码的整洁和条理性,还有对逻辑use case的划分,这样便于单元测试,保证代码的质量。

以上为本身的学习笔记,若有理解错误的地方,还请你们指出,谢谢!

相关文章
相关标签/搜索