
關注倉庫,及時獲得更新:iOS-Source-Code-Analyze
MBProgressHUD是一個為iOS app添加透明浮層 HUD 的第三方框架。作為一個 UI 層面的框架,它的實現很簡單,但是其中也有一些非常有意思的代碼。
MBProgressHUD
MBProgressHUD是一個 UIView 的子類,它提供了一系列的創建 HUD 的方法。我們在這裡會主要介紹三種使用 HUD 的方法。
+ showHUDAddedTo:animated:
- showAnimated:whileExecutingBlock:onQueue:completionBlock:
- showWhileExecuting:onTarget:withObject:
+ showHUDAddedTo:animated:
MBProgressHUD 提供了一對類方法 + showHUDAddedTo:animated: 和 + hideHUDForView:animated: 來創建和隱藏 HUD, 這是創建和隱藏 HUD 最簡單的一組方法
+ (MB_INSTANCETYPE)showHUDAddedTo:(UIView *)view animated:(BOOL)animated {
MBProgressHUD *hud = [[self alloc] initWithView:view];
hud.removeFromSuperViewOnHide = YES;
[view addSubview:hud];
[hud show:animated];
return MB_AUTORELEASE(hud);
}- initWithView:
首先調用 + alloc - initWithView: 方法返回一個 MBProgressHUD 的實例, - initWithView: 方法會調用當前類的 - initWithFrame: 方法。
通過 - initWithFrame: 方法的執行,會為 MBProgressHUD 的一些屬性設置一系列的默認值。
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Set default values for properties
self.animationType = MBProgressHUDAnimationFade;
self.mode = MBProgressHUDModeIndeterminate;
...
// Make it invisible for now
self.alpha = 0.0f;
[self registerForKVO];
...
}
return self;
}在 MBProgressHUD 初始化的過程中, 有一個需要注意的方法 - registerForKVO, 我們會在之後查看該方法的實現。
- show:
在初始化一個 HUD 並添加到 view 上之後, 這時 HUD 並沒有顯示出來, 因為在初始化時, view.alpha 被設置為 0。所以我們接下來會調用 - show: 方法使 HUD 顯示到屏幕上。
- (void)show:(BOOL)animated {
NSAssert([NSThread isMainThread], @"MBProgressHUD needs to be accessed on the main thread.");
useAnimation = animated;
// If the grace time is set postpone the HUD display
if (self.graceTime > 0.0) {
NSTimer *newGraceTimer = [NSTimer timerWithTimeInterval:self.graceTime target:self selector:@selector(handleGraceTimer:) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:newGraceTimer forMode:NSRunLoopCommonModes];
self.graceTimer = newGraceTimer;
}
// ... otherwise show the HUD imediately
else {
[self showUsingAnimation:useAnimation];
}
}因為在 iOS 開發中,對於 UIView 的處理必須在主線程中, 所以在這裡我們要先用 [NSThread isMainThread] 來確認當前前程為主線程。
如果 graceTime 為 0,那麼直接調用 - showUsingAnimation: 方法, 否則會創建一個 newGraceTimer 當然這個 timer 對應的 selector 最終調用的也是 - showUsingAnimation: 方法。
- showUsingAnimation:
- (void)showUsingAnimation:(BOOL)animated {
// Cancel any scheduled hideDelayed: calls
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self setNeedsDisplay];
if (animated && animationType == MBProgressHUDAnimationZoomIn) {
self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(0.5f, 0.5f));
} else if (animated && animationType == MBProgressHUDAnimationZoomOut) {
self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(1.5f, 1.5f));
}
self.showStarted = [NSDate date];
// Fade in
if (animated) {
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.30];
self.alpha = 1.0f;
if (animationType == MBProgressHUDAnimationZoomIn || animationType == MBProgressHUDAnimationZoomOut) {
self.transform = rotationTransform;
}
[UIView commitAnimations];
}
else {
self.alpha = 1.0f;
}
}這個方法的核心功能就是根據 animationType 為 HUD 的出現添加合適的動畫。
typedef NS_ENUM(NSInteger, MBProgressHUDAnimation) {
/** Opacity animation */
MBProgressHUDAnimationFade,
/** Opacity + scale animation */
MBProgressHUDAnimationZoom,
MBProgressHUDAnimationZoomOut = MBProgressHUDAnimationZoom,
MBProgressHUDAnimationZoomIn
};它在方法剛調用時會通過 - cancelPreviousPerformRequestsWithTarget: 移除附加在 HUD 上的所有 selector, 這樣可以保證該方法不會多次調用。
同時也會保存 HUD 的出現時間。
self.showStarted = [NSDate date]
+ hideHUDForView:animated:
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated {
MBProgressHUD *hud = [self HUDForView:view];
if (hud != nil) {
hud.removeFromSuperViewOnHide = YES;
[hud hide:animated];
return YES;
}
return NO;
}+ hideHUDForView:animated: 方法的實現和 + showHUDAddedTo:animated: 差不多, + HUDForView: 方法會返回對應 view 最上層的 MBProgressHUD 的實例。
+ (MB_INSTANCETYPE)HUDForView:(UIView *)view {
NSEnumerator *subviewsEnum = [view.subviews reverseObjectEnumerator];
for (UIView *subview in subviewsEnum) {
if ([subview isKindOfClass:self]) {
return (MBProgressHUD *)subview;
}
}
return nil;
}然後調用的 - hide: 方法和 - hideUsingAnimation: 方法也沒有什麼特別的, 只有在 HUD 隱藏之後 - done 負責隱藏執行 completionBlock 和 delegate 回調。
- (void)done {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
isFinished = YES;
self.alpha = 0.0f;
if (removeFromSuperViewOnHide) {
[self removeFromSuperview];
}
#if NS_BLOCKS_AVAILABLE
if (self.completionBlock) {
self.completionBlock();
self.completionBlock = NULL;
}
#endif
if ([delegate respondsToSelector:@selector(hudWasHidden:)]) {
[delegate performSelector:@selector(hudWasHidden:) withObject:self];
}
}- showAnimated:whileExecutingBlock:onQueue:completionBlock:
當 block 指定的隊列執行時, 顯示 HUD, 並在 HUD 消失時, 調用 completion。
同時 MBProgressHUD 也提供一些其他的便利方法實現這一功能:
- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block; - (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block completionBlock:(MBProgressHUDCompletionBlock)completion; - (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue;
該方法會異步在指定 queue 上運行 block 並在 block 執行結束調用 - cleanUp。
- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue
completionBlock:(MBProgressHUDCompletionBlock)completion {
self.taskInProgress = YES;
self.completionBlock = completion;
dispatch_async(queue, ^(void) {
block();
dispatch_async(dispatch_get_main_queue(), ^(void) {
[self cleanUp];
});
});
[self show:animated];
}關於 - cleanUp 我們會在下一段中介紹。
- showWhileExecuting:onTarget:withObject:
當一個後台任務在新線程中執行時,顯示 HUD。
- (void)showWhileExecuting:(SEL)method onTarget:(id)target withObject:(id)object animated:(BOOL)animated {
methodForExecution = method;
targetForExecution = MB_RETAIN(target);
objectForExecution = MB_RETAIN(object);
// Launch execution in new thread
self.taskInProgress = YES;
[NSThread detachNewThreadSelector:@selector(launchExecution) toTarget:self withObject:nil];
// Show HUD view
[self show:animated];
}在保存 methodForExecution targetForExecution 和 objectForExecution 之後, 會在新的線程中調用方法。
- (void)launchExecution {
@autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
// Start executing the requested task
[targetForExecution performSelector:methodForExecution withObject:objectForExecution];
#pragma clang diagnostic pop
// Task completed, update view in main thread (note: view operations should
// be done only in the main thread)
[self performSelectorOnMainThread:@selector(cleanUp) withObject:nil waitUntilDone:NO];
}
}- launchExecution 會創建一個自動釋放池, 然後再這個自動釋放池中調用方法, 並在方法調用結束之後在主線程執行 - cleanUp。
Trick
在 MBProgressHUD 中有很多神奇的魔法來解決一些常見的問題。
ARC
MBProgressHUD 使用了一系列神奇的宏定義來兼容 MRC。
#ifndef MB_INSTANCETYPE #if __has_feature(objc_instancetype) #define MB_INSTANCETYPE instancetype #else #define MB_INSTANCETYPE id #endif #endif #ifndef MB_STRONG #if __has_feature(objc_arc) #define MB_STRONG strong #else #define MB_STRONG retain #endif #endif #ifndef MB_WEAK #if __has_feature(objc_arc_weak) #define MB_WEAK weak #elif __has_feature(objc_arc) #define MB_WEAK unsafe_unretained #else #define MB_WEAK assign #endif #endif
通過宏定義 __has_feature 來判斷當前環境是否啟用了 ARC, 使得不同環境下宏不會出錯。
KVO
MBProgressHUD 通過 @property 生成了一系列的屬性。
- (NSArray *)observableKeypaths {
return [NSArray arrayWithObjects:@"mode", @"customView", @"labelText", @"labelFont", @"labelColor",
@"detailsLabelText", @"detailsLabelFont", @"detailsLabelColor", @"progress", @"activityIndicatorColor", nil];
}這些屬性在改變的時候不會, 重新渲染整個 view, 我們在一般情況下覆寫 setter 方法, 然後再 setter 方法中刷新對應的屬性,在 MBProgressHUD 中使用 KVO 來解決這個問題。
- (void)registerForKVO {
for (NSString *keyPath in [self observableKeypaths]) {
[self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL];
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:@selector(updateUIForKeypath:) withObject:keyPath waitUntilDone:NO];
} else {
[self updateUIForKeypath:keyPath];
}
}
- (void)updateUIForKeypath:(NSString *)keyPath {
if ([keyPath isEqualToString:@"mode"] || [keyPath isEqualToString:@"customView"] ||
[keyPath isEqualToString:@"activityIndicatorColor"]) {
[self updateIndicators];
} else if ([keyPath isEqualToString:@"labelText"]) {
label.text = self.labelText;
} else if ([keyPath isEqualToString:@"labelFont"]) {
label.font = self.labelFont;
} else if ([keyPath isEqualToString:@"labelColor"]) {
label.textColor = self.labelColor;
} else if ([keyPath isEqualToString:@"detailsLabelText"]) {
detailsLabel.text = self.detailsLabelText;
} else if ([keyPath isEqualToString:@"detailsLabelFont"]) {
detailsLabel.font = self.detailsLabelFont;
} else if ([keyPath isEqualToString:@"detailsLabelColor"]) {
detailsLabel.textColor = self.detailsLabelColor;
} else if ([keyPath isEqualToString:@"progress"]) {
if ([indicator respondsToSelector:@selector(setProgress:)]) {
[(id)indicator setValue:@(progress) forKey:@"progress"];
}
return;
}
[self setNeedsLayout];
[self setNeedsDisplay];
}- observeValueForKeyPath:ofObject:change:context: 方法中的代碼是為了保證 UI 的更新一定是在主線程中, 而 - updateUIForKeypath: 方法負責 UI 的更新。
End
MBProgressHUD 由於是一個UI的第三方庫,所以它的實現還是挺簡單的。