阅读 760

读MBProgressHUD

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

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

涉及到的类

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

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

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

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

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

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


我们主要来看下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的划分,这样便于单元测试,保证代码的质量。

以上为自己的学习笔记,如有理解错误的地方,还请大家指出,谢谢!

关注下面的标签,发现更多相似文章
评论