
1.前言
MBProgressHUD 是 iOS 開發(fā)中經(jīng)常會(huì)用到的一個(gè)加載動(dòng)畫庫,本文就來簡單學(xué)習(xí)一下源碼。
2.視圖層級(jí)
在開始學(xué)習(xí)源碼之前,先大概了解一下整個(gè)視圖的層級(jí)結(jié)構(gòu)吧,主要分這幾個(gè)視圖:

-
backgroundView,位于最底層,是一個(gè)遮罩層,在 HUD 顯示時(shí),我們之所以無法點(diǎn)擊后邊的視圖,都是因?yàn)樗?/li> -
bezelView,位于 backgroundView 之上,承載著下邊要說的幾個(gè)視圖,下邊 4 個(gè)視圖位于同一層級(jí)。 -
indicator,是 bezelView 的子視圖,類型不定,他自己提供了 2 種類型MBRoundProgressView和MBBarProgressView,不過也可以使用用戶自定義視圖customView。 -
label,indicator 下方的提示標(biāo)簽。 -
detailLabel,label 下方的補(bǔ)充提示標(biāo)簽。 -
button,最底部的按鈕,可以響應(yīng)點(diǎn)擊事件。
當(dāng)然,以上這些視圖不一定要全部顯示,可以由用戶(我們自己 O(∩_∩)O )自由選擇。
3.源碼
言歸正傳,現(xiàn)在開始讀源碼。
3.1 類結(jié)構(gòu)
雖然 MBProgressHUD 的文件數(shù)量非常少,只有 2 個(gè):MBProgressHUD.h 和 MBProgressHUD.m,但還有幾個(gè)相關(guān)的類和協(xié)議:

3.2 MBProgressHUD
我們主要研究主類 MBProgressHUD,首先看幾個(gè)重要的屬性:
@property (assign, nonatomic) NSTimeInterval graceTime; // 顯示的寬限時(shí)間,即從顯示方法被調(diào)用到真正顯示之間的時(shí)間差
@property (assign, nonatomic) NSTimeInterval minShowTime; // HUD 最小的展示時(shí)間
@property (nonatomic, weak) NSTimer *graceTimer; // 延遲顯示的 timer
@property (nonatomic, weak) NSTimer *minShowTimer; // 保證 HUD 最短顯示維持時(shí)間 的timer
@property (nonatomic, weak) NSTimer *hideDelayTimer; // 延遲隱藏的 timer
接下來看看 MBProgressHUD 為我們提供的 3 個(gè)類方法用于展示和隱藏 HUD 的方法:
// 展示 HUD
+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated;
// 隱藏 HUD
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated;
// 返回最頂層未結(jié)束的 HUD
+ (nullable MBProgressHUD *)HUDForView:(UIView *)view;
我們從展示的方法實(shí)現(xiàn)開始探究,實(shí)現(xiàn)代碼如下:
+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated {
MBProgressHUD *hud = [[self alloc] initWithView:view];
hud.removeFromSuperViewOnHide = YES;
[view addSubview:hud];
[hud showAnimated:animated];
return hud;
}
我們發(fā)現(xiàn),在 initWithView: 方法中對(duì)入?yún)?view 做了空判斷,這里使用了 NSAsset,如果為空,會(huì)報(bào)錯(cuò):
- (id)initWithView:(UIView *)view {
NSAssert(view, @"View must not be nil.");
return [self initWithFrame:view.bounds];
}
接著看 showAnimated: 的方法實(shí)現(xiàn)。
- (void)showAnimated:(BOOL)animated {
// 1.確保在主線程執(zhí)行
MBMainThreadAssert();
// 2.關(guān)閉之前可能存在的 minShowTimer
[self.minShowTimer invalidate];
self.useAnimation = animated;
self.finished = NO;
// 3.是否延緩 HUD 的展示
// 如果設(shè)置了 graceTime,則延緩 HUD 的展示
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;
} else {
// 如果沒設(shè)置 graceTime,則立即展示 HUD
[self showUsingAnimation:self.useAnimation];
}
}
// 定時(shí)器 graceTimer 的響應(yīng)方法
- (void)handleGraceTimer:(NSTimer *)theTimer {
// 只有當(dāng)任務(wù)還在進(jìn)行的時(shí)候才會(huì)顯示 HUD,否則什么也不做
if (!self.hasFinished) {
[self showUsingAnimation:self.useAnimation];
}
}
此處做了 3 件事:
① 首先,為了確保在主線程執(zhí)行,能夠及時(shí)更新 UI ,這里使用了 NSAsset,如果當(dāng)前線程不是主線程,就會(huì)報(bào)錯(cuò),MBMainThreadAssert() 的定義如下。
#define MBMainThreadAssert() NSAssert([NSThread isMainThread], @"MBProgressHUD needs to be accessed on the main thread.");
② 然后,移除了可能存在的 minShowTimer。
③ 最后,根據(jù) graceTime 有無值來決定是否需要延緩 HUD 的顯示,如果有值,則啟動(dòng) graceTimer,待時(shí)間到時(shí)再執(zhí)行顯示的操作;如果無值,則直接去顯示 HUD。
graceTime 應(yīng)該是針對(duì)耗時(shí)比較少的操作準(zhǔn)備的,定時(shí)器 graceTimer 時(shí)間到的時(shí)候,操作有可能已經(jīng)完成了 (self.hasFinished == YES),這時(shí)就不需要展示 HUD 了,以免影響用戶體驗(yàn)。
另外,minShowTime 是在隱藏的時(shí)候使用的,通過 minShowTimer 保證 HUD 展示時(shí)間 >= minShowTime 從而避免 HUD 顯示時(shí)間過短的問題,也是為了提升用戶體驗(yàn)。
做完 ③ 的判斷就該去顯示了,即 showUsingAnimation: 的實(shí)現(xiàn),它主要也做了 3 件事:
- (void)showUsingAnimation:(BOOL)animated {
[self.bezelView.layer removeAllAnimations];
[self.backgroundView.layer removeAllAnimations];
[self.hideDelayTimer invalidate];
self.showStarted = [NSDate date];
self.alpha = 1.f;
[self setNSProgressDisplayLinkEnabled:YES];
if (animated) {
[self animateIn:YES withType:self.animationType completion:NULL];
} else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
self.bezelView.alpha = self.opacity;
#pragma clang diagnostic pop
self.backgroundView.alpha = 1.f;
}
}
① 取消之前可能存在的 animations 并停止 ‘延緩隱藏’ 的 timer。
② 根據(jù)是否需要展示進(jìn)度做相應(yīng)的處理。此處傳的是 YES,即需要展示:創(chuàng)建一個(gè) CADisplayLink對(duì)象并啟動(dòng)(啟動(dòng)的代碼寫在了 progressObjectDisplayLink 的 setter 里邊),在響應(yīng)方法里將外界傳入的 progressObject(NSProgress)的值 progressObject.fractionCompleted 賦給 progress(CGFloat),然后在 progress 的 setter 里更新控件的值。
// 創(chuàng)建 CADisplayLink 對(duì)象,用于更新
- (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled {
if (enabled && self.progressObject) {
if (!self.progressObjectDisplayLink) {
self.progressObjectDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateProgressFromProgressObject)];
}
} else {
self.progressObjectDisplayLink = nil;
}
}
// 為 progressObjectDisplayLink 賦值,并啟動(dòng) CADisplayLink
- (void)setProgressObjectDisplayLink:(CADisplayLink *)progressObjectDisplayLink {
if (progressObjectDisplayLink != _progressObjectDisplayLink) {
[_progressObjectDisplayLink invalidate];
_progressObjectDisplayLink = progressObjectDisplayLink;
[_progressObjectDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
}
// 給 progress 賦值
- (void)updateProgressFromProgressObject {
self.progress = self.progressObject.fractionCompleted;
}
// progress 的 setter,并給展示進(jìn)度的控件賦值
- (void)setProgress:(float)progress {
if (progress != _progress) {
_progress = progress;
UIView *indicator = self.indicator;
if ([indicator respondsToSelector:@selector(setProgress:)]) {
[(id)indicator setValue:@(self.progress) forKey:@"progress"];
}
}
}
外界給 _progressObject 賦值的方法實(shí)現(xiàn)如下。這里加了 if (progressObject != _progressObject) 的判斷,避免了重復(fù)賦值。
- (void)setProgressObject:(NSProgress *)progressObject {
if (progressObject != _progressObject) {
_progressObject = progressObject;
[self setNSProgressDisplayLinkEnabled:YES];
}
}
③ 如果需要過渡動(dòng)畫,無論是 show 還是 hide 都會(huì)調(diào)用 animateIn: withType: completion: 這個(gè)方法,代碼實(shí)現(xiàn)如下,詳見注釋。
- (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion {
// 確定縮放動(dòng)畫的類型
if (type == MBProgressHUDAnimationZoom) {
type = animatingIn ? MBProgressHUDAnimationZoomIn : MBProgressHUDAnimationZoomOut;
}
CGAffineTransform small = CGAffineTransformMakeScale(0.5f, 0.5f); // x、y 方向的縮放倍數(shù)均為 0.5
CGAffineTransform large = CGAffineTransformMakeScale(1.5f, 1.5f); // x、y 方向的縮放倍數(shù)均為 1.5
UIView *bezelView = self.bezelView;
// * 設(shè)置初始狀態(tài)的值(show 的 過渡動(dòng)畫 的初值,hide 的初值就不用設(shè)置了)
if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomIn) {
bezelView.transform = small;
} else if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomOut) {
bezelView.transform = large;
}
// 執(zhí)行動(dòng)畫的 block,作為后邊方法的參數(shù)
dispatch_block_t animations = ^{
// * show 的過渡動(dòng)畫 的 終值
if (animatingIn) {
bezelView.transform = CGAffineTransformIdentity;
// * 下邊 2 個(gè) if 均為 hide 的過渡動(dòng)畫 的 終值
} else if (!animatingIn && type == MBProgressHUDAnimationZoomIn) {
bezelView.transform = large;
} else if (!animatingIn && type == MBProgressHUDAnimationZoomOut) {
bezelView.transform = small;
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
bezelView.alpha = animatingIn ? self.opacity : 0.f;
#pragma clang diagnostic pop
self.backgroundView.alpha = animatingIn ? 1.f : 0.f;
};
// 此方法 iOS 7.0 之后才支持
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 || TARGET_OS_TV
if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_7_0) {
[UIView animateWithDuration:0.3
delay:0.
usingSpringWithDamping:1.f
initialSpringVelocity:0.f
options:UIViewAnimationOptionBeginFromCurrentState
animations:animations
completion:completion];
return;
}
#endif
// iOS 4.0 就開始支持
[UIView animateWithDuration:0.3
delay:0.
options:UIViewAnimationOptionBeginFromCurrentState
animations:animations
completion:completion];
}
隱藏的邏輯與此類似,可自行參看代碼,這里簡單繪制了一張方法調(diào)用流程圖作為小結(jié),如下所示:
