讀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的劃分,這樣便於單元測試,保證代碼的質量。

以上爲本身的學習筆記,若有理解錯誤的地方,還請你們指出,謝謝!

相關文章
相關標籤/搜索