日常的開發中,咱們一般會在處理一些耗時任務的時候,在界面上顯示一個加載的標識(或者一個進度條),這樣會讓用戶知道app是在作事情,而不是像卡死了的一動不動的中止在那裏。git
MBProgressHUD
:做爲iOS開發的開發者,即使沒用過,也應該聽過,做爲加載動畫的第三方,我本身對這個庫的感受就是簡單、好用。以前也看過MBProgressHUD
的源碼,當時看了,以爲做者的實現很簡單,自定義一個view,有一個展現和收起的方法。就直接封裝了一層,開始用了。如今回頭來,再看,除了看做者如何實現以外,我還有了其它的收穫,尤爲是做者代碼的佈局,清晰易懂,看到做者的代碼後,筆者本身直觀的感覺就是很暢快、願意看下去。github
首先咱們來看下這個庫涉及到的類:bash
MBProgressHUD:核心類,咱們在外部直接調用這個類,生成這個類的一個實例,而後加到咱們想要加到的view
或window
上。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
方法中去掉。這裏很清晰地成對的對通知進行添加和去除,從而避免忘了去除觀察者。學習
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設置了不一樣的顏色。
MBProgressHUD
的實例hud。MBBackgroundView
的實例,做爲hud的背景視圖backgroundView
。MBBackgroundView
類的實例bezelView
,做爲真正顯示loading圖的容器view。該視圖上佈局label
、detailLabel
、indicator
、和button
。其中indicator
做爲私有屬性,根據設置hud的mode
的不一樣,從而設置不一樣的indicator
,所以做者設置屬性的時候將indicator
的屬性類型設置爲UIView。在這裏要提醒注意的是:若是要設置mode爲MBProgressHUDModeCustomView
,就是你不想用第三方提供的一些指示器視圖,想本身自定的話,你自定義的view必須實現intrinsicContentSize
方法,得到一個合適的大小。由於做者的佈局是經過約束,不是利用frame的,做者在方法的註釋裏也說明了,該自定義視圖須要實現intrinsicContentSize
固有大小的方法來得到一個合適的尺寸。系統中的UILabel
、UIButton
和UIImageView
都默認已經實現了intrinsicContentSize
這個方法,若是你的自定義view就直接是繼承於UIView類的話,那麼須要實現這個intrinsicContentSize
方法。
@property (nonatomic, weak) NSTimer *graceTimer;
@property (nonatomic, weak) NSTimer *minShowTimer;
@property (nonatomic, weak) NSTimer *hideDelayTimer;
@property (nonatomic, weak) CADisplayLink *progressObjectDisplayLink;
複製代碼
咱們就從這四個定時器的使用來查看做者的實現:
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];
}
}
複製代碼
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];
}
複製代碼
- (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
定時器棄用,再以新的定時器來開始計時延時隱藏。
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
的劃分,這樣便於單元測試,保證代碼的質量。
以上爲本身的學習筆記,若有理解錯誤的地方,還請你們指出,謝謝!