本篇博文記錄MBProgressHUD原始碼學習過程,從官方提供的Demo專案入手,一步步瞭解其程式碼結構,學習它使用的技術,體會作者的程式設計思想。
一、結構
我們先來看下MBProgressHUD的結構,檢視其類的定義。
1.MBProgressHUD是UIView的子類。
2.屬性:
1.
//代理,<MBProgressHUDDelegate>僅定義了一個方法:- (void)hudWasHidden:(MBProgressHUD *)hud;用於執行HUD隱藏之後的操作
@property (weak, nonatomic) id<MBProgressHUDDelegate> delegate;
//執行HUD隱藏之後的操作的Block,目的同上
@property (copy, nullable) MBProgressHUDCompletionBlock completionBlock;
2.
//延遲時間,若任務在graceTime到時之前就完成了,HUD不再展示,即防止為短時間任務顯示HUD
@property (assign, nonatomic) NSTimeInterval graceTime;
//最短展示時間,防止HUD隱藏的過快
@property (assign, nonatomic) NSTimeInterval minShowTime;
//配置HUD是否隱藏之後就從其superview上移除。預設NO
@property (assign, nonatomic) BOOL removeFromSuperViewOnHide;
3.
//指定進度條的樣式,包括菊花、圓餅、環形、水平進度條、自定義樣式和純文字等
@property (assign, nonatomic) MBProgressHUDMode mode;
//內容(label+indicator+customView)顏色
@property (strong, nonatomic, nullable) UIColor *contentColor;
//顯示和隱藏時的動畫型別:Fade(淡入淡出)、Zoom(放大顯示縮小隱藏)、ZoomIn、ZoomOut
@property (assign, nonatomic) MBProgressHUDAnimation animationType;
//內容框(bezelView)距離中心位置的偏移,例如CGPointMake(0.f, MBProgressMaxOffset),內容框會在底部居中
@property (assign, nonatomic) CGPoint offset;
@property (assign, nonatomic) CGFloat margin;//各元素到HUD的邊距
@property (assign, nonatomic) CGSize minSize;//內容框的最小尺寸
@property (assign, nonatomic, getter = isSquare) BOOL square;//強制HUD為方形
@property (assign, nonatomic, getter=areDefaultMotionEffectsEnabled) BOOL defaultMotionEffectsEnabled;//內容框(bezelView)是否受裝置加速計的影響,預設YES
4.
@property (assign, nonatomic) float progress;//進度
@property (strong, nonatomic, nullable) NSProgress *progressObject;//進度物件,用於更新進度條
5.
//內容框,即展示實際內容(文字、indicator)的矩形框
@property (strong, nonatomic, readonly) MBBackgroundView *bezelView;
//背景試圖,會覆蓋整個螢幕
@property (strong, nonatomic, readonly) MBBackgroundView *backgroundView;
//自定義檢視用於展示
@property (strong, nonatomic, nullable) UIView *customView;
@property (strong, nonatomic, readonly) UILabel *label;//文字
@property (strong, nonatomic, readonly) UILabel *detailsLabel;//文字下面的詳細文字
@property (strong, nonatomic, readonly) UIButton *button;//文字下面的action button
3.其他相關類
(1) MBBackgroundView
- UIView的子類,在MBPRogressHUD中充當內容框(bezelView)和背景檢視(backgroundView)兩種角色。
- 提供兩種樣式:清晰樣式(MBProgressHUDBackgroundStyleSolidColor)和模糊樣式(MBProgressHUDBackgroundStyleBlur)
- 模糊樣式是通過
UIVisualEffectView
和UIBlurEffect
實現的。
(2) MBRoundProgressView
- UIView的子類,展示為餅狀/環形的進度條。
(3) MBBarProgressView
- UIView的子類,展示為條狀的進度條。
(4) MBProgressHUDRoundedButton
- UIButton的子類,展示位圓角button,作為HUD上的功能按鈕。
知識點:HUD中有個button屬性如下:
/**
* A button that is placed below the labels. Visible only if a target / action is added.
*/
@property (strong, nonatomic, readonly) UIButton *button;
注意它的註釋Visible only if a target / action is added
。也就是說,只有給button新增事件之後,該按鈕才會展示出來。這是如何做到的呢?那就是重寫UIView的函式- (CGSize)intrinsicContentSize
:
- (CGSize)intrinsicContentSize {
// Only show if we have associated control events
if (self.allControlEvents == 0) return CGSizeZero;
CGSize size = [super intrinsicContentSize];
// Add some side padding
size.width += 20.f;
return size;
}
這個函式用來設定控制元件的內建尺寸。可以看到,通過判斷allControlEvents
的個數來判斷button上是否有事件,如果有事件,就在原來內建的尺寸上加20。
二、程式碼追蹤
瞭解了MBProgressHUD的基本結構之後,接下來我們就看看具體的功能是如何實現的。HUDDemo提供了15個樣例,我們選取純文字、載入(菊花)、條狀進度條和自定義檢視進行分析,其他的樣例與它們類似。
1.純文字(Text)
我們先從最簡單的純文字開始。啟動HUDDemo專案,點開MBHudDemoViewController.m
檔案,找到函式- (void)textExample{…}
,這個函式就是顯示純文字的處理函式:
- (void)textExample {
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
// Set the text mode to show only text.
hud.mode = MBProgressHUDModeText;
hud.label.text = NSLocalizedString(@"Message here!", @"HUD message title");
// Move to bottm center.
hud.offset = CGPointMake(0.f, MBProgressMaxOffset);
[hud hideAnimated:YES afterDelay:3.f];
}
① 進入到函式showHUDAddedTo:animated:
中檢視MBProgressHUD例項的建立過程:
initWithView:
->initWithFrame:
->commonInit
使用
self.navigationController.view
的bounds初始化HUD,然後在commonInit
裡指定動畫型別(Fade)、HUD模式(菊花)、間距(20)、內容顏色(黑色半透明)。除此之外,還設定HUD為完全透明,背景色為clear,配置HUD的尺寸自動調整://保證上下間距比例不變、左右間距比例不變,即防止橫豎屏切換時HUD位置錯誤 self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; //讓HUD的各個子檢視自己控制自己的透明度,使其不受HUD透明度的影響 self.layer.allowsGroupOpacity = NO;
[self setupViews]
在這個函式中真正執行子檢視的建立工作。
背景檢視(backgroundView)
為類
MBBackgroundView
的例項。MBBackgroundView
例項預設會建立成白色半透明模糊效果,並覆蓋全屏,但在本例中,建立完成之後會更改其style
為MBProgressHUDBackgroundStyleSolidColor
,並將背景色設定為透明(clear)。內容框(bezelView)
同為類
MBBackgroundView
例項,是實際展示內容的View(即中間的黑框),包含文字、indicator、進度條等。bezelView
會預設建立成白色半透明模糊效果,但frame為0。建立後會設定其邊角半徑為5。知識點:作者為bezelView新增了MotionEffect,也就是說在bezelView顯示的時候,它會根據手機的傾斜方向調整自己的位置!
- (void)updateBezelMotionEffects { #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 || TARGET_OS_TV MBBackgroundView *bezelView = self.bezelView; if (![bezelView respondsToSelector:@selector(addMotionEffect:)]) return; if (self.defaultMotionEffectsEnabled) { CGFloat effectOffset = 10.f; UIInterpolatingMotionEffect *effectX = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.x" type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis]; effectX.maximumRelativeValue = @(effectOffset); effectX.minimumRelativeValue = @(-effectOffset); UIInterpolatingMotionEffect *effectY = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.y" type:UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis]; effectY.maximumRelativeValue = @(effectOffset); effectY.minimumRelativeValue = @(-effectOffset); UIMotionEffectGroup *group = [[UIMotionEffectGroup alloc] init]; group.motionEffects = @[effectX, effectY]; [bezelView addMotionEffect:group]; } else { NSArray *effects = [bezelView motionEffects]; for (UIMotionEffect *effect in effects) { [bezelView removeMotionEffect:effect]; } } #endif }
label和detailsLabel
設定顯示文字的label,其中detailsLabel允許多行。
button
為
MBProgressHUDRoundedButton
的例項,作為HUD上的功能按鈕,比如進度條下方可以顯示一個"取消"按鈕。topSpacer和bottomSpacer
均為
UIView
的例項,用於調節上下間距的輔助View。設定label、detailsLabel及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]; }
[self updateIndicators]
HUD的
indicator
是UIView
的例項,用來記錄HUD上顯示的檢視,進度條、載入圖示(菊花)、自定義檢視等都是用HUD的indicator
屬性記錄的。在函式- (void)updaetIndicators
中,根據HUD的mode值配置不同的indicator。最後會呼叫[self setNeedsUpdateConstraints]
觸發約束更新函式-(void)updateConstraints
來更新UI。[self registerForNotifications]
註冊通知,處理螢幕旋轉的問題。
② 在建立完HUD之後,會呼叫[hud showAnimated:animated];
將HUD展示到螢幕上。事實上,雖然當前HUD已經在螢幕上了,但由於初始化HUD的時候bezelView的frame為0,使用者看不到。
③ 配置HUD例項的屬性
hud.mode = MBProgressHUDModeText;//設定hud只顯示純文字
hud.label.text = NSLocalizedString(@"Message here!", @"HUD message title");//設定文字內容
hud.offset = CGPointMake(0.f, MBProgressMaxOffset);//設定hud相對於中心位置的偏移
在mode的setter函式中會呼叫- (void)updateIndicators
,根據mode的新值重新配置indicator,然後呼叫- (void)setNeedsUpdateConstraints
觸發-(void)updateConstraints
來更新UI。而在offset的setter函式中會直接呼叫- (void)setNeedsUpdateConstraints
觸發-(void)updateConstraints
來更新UI。
④ 在函式- (void)updateConstraints
中更新佈局:
- 刪除所有控制元件的constraints
- 通過
NSLayoutConstraint
重新設定constraints
//1.以螢幕中心為基準,應用offset。priority = 998
CGPoint offset = self.offset;
NSMutableArray *centeringConstraints = [NSMutableArray array];
//x
[centeringConstraints addObject:[NSLayoutConstraint constraintWithItem:bezel attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.f constant:offset.x]];
//y
[centeringConstraints addObject:[NSLayoutConstraint constraintWithItem:bezel attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1.f constant:offset.y]];
//為每個constraints設定priority
[self applyPriority:998.f toConstraints:centeringConstraints];
[self addConstraints:centeringConstraints];
//2.設定最小間距約束,priority = 999
NSMutableArray *sideConstraints = [NSMutableArray array];
[sideConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"|-(>=margin)-[bezel]-(>=margin)-|" options:0 metrics:metrics views:NSDictionaryOfVariableBindings(bezel)]];
[sideConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=margin)-[bezel]-(>=margin)-|" options:0 metrics:metrics views:NSDictionaryOfVariableBindings(bezel)]];
[self applyPriority:999.f toConstraints:sideConstraints];
[self addConstraints:sideConstraints];
//3.bezel的最小尺寸約束 priority = 997
CGSize minimumSize = self.minSize;
if (!CGSizeEqualToSize(minimumSize, CGSizeZero)) {
NSMutableArray *minSizeConstraints = [NSMutableArray array];
[minSizeConstraints addObject:[NSLayoutConstraint constraintWithItem:bezel attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.f constant:minimumSize.width]];
[minSizeConstraints addObject:[NSLayoutConstraint constraintWithItem:bezel attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.f constant:minimumSize.height]];
[self applyPriority:997.f toConstraints:minSizeConstraints];
[bezelConstraints addObjectsFromArray:minSizeConstraints];
}
//4.方形約束 priority=997
if (self.square) {
NSLayoutConstraint *square = [NSLayoutConstraint constraintWithItem:bezel attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:bezel attribute:NSLayoutAttributeWidth multiplier:1.f constant:0];
square.priority = 997.f;
[bezelConstraints addObject:square];
}
//5.根據margin和設定上下spacer的間距約束
[topSpacer addConstraint:[NSLayoutConstraint constraintWithItem:topSpacer attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.f constant:margin]];
[bottomSpacer addConstraint:[NSLayoutConstraint constraintWithItem:bottomSpacer attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.f constant:margin]];
[bezelConstraints addObject:[NSLayoutConstraint constraintWithItem:topSpacer attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:bottomSpacer attribute:NSLayoutAttributeHeight multiplier:1.f constant:0.f]];
//6.設定bezel子檢視(topSpacer、label、detailLabel、button、bottomSpacer)的約束
[subviews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL *stop) {
// Center in bezel
[bezelConstraints addObject:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:bezel attribute:NSLayoutAttributeCenterX multiplier:1.f constant:0.f]];
// Ensure the minimum edge margin is kept
[bezelConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"|-(>=margin)-[view]-(>=margin)-|" options:0 metrics:metrics views:NSDictionaryOfVariableBindings(view)]];
// Element spacing
if (idx == 0) {
// First, ensure spacing to bezel edge
[bezelConstraints addObject:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:bezel attribute:NSLayoutAttributeTop multiplier:1.f constant:0.f]];
} else if (idx == subviews.count - 1) {
// Last, ensure spacing to bezel edge
[bezelConstraints addObject:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:bezel attribute:NSLayoutAttributeBottom multiplier:1.f constant:0.f]];
}
if (idx > 0) {
// Has previous
NSLayoutConstraint *padding = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:subviews[idx - 1] attribute:NSLayoutAttributeBottom multiplier:1.f constant:0.f];
[bezelConstraints addObject:padding];
[paddingConstraints addObject:padding];
}
}];
[bezel addConstraints:bezelConstraints];
self.bezelConstraints = bezelConstraints;
self.paddingConstraints = [paddingConstraints copy];
[self updatePaddingConstraints];//在該函式裡,根據子檢視的可視性(hidden),設定子檢視的上下間距(為4)
通過上面的priority可以知道優先順序:最小間距約束>bezel的偏移約束>bezel最小尺寸約束=方形約束。因此,如果你設定了hud.square = YES
,但是實際bezel並沒有變為方形,則很可能是因為上面的這幾個約束之間存在衝突,系統採用了高優先順序的約束而忽略了square約束。不信你可以把square優先順序改為1000試試看:)
知識點:這裡出現了一個巨集NSDictionaryOfVariableBindings
,它可以用來方便的建立NSDictionary:
UIView *view1 = [UIView new];
UIView *view2 = [UIView new];
NSDictionary *dict = NSDictionaryOfVariableBindings(view1,view2);//{@"view1":view1,@"view2":view2}
總結:
至此,我們來總結下純文字HUD的整個建立及顯示流程:
- 呼叫
[MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES]
建立HUD例項:包括配置屬性預設值(動畫型別、HUD樣式、間距、內容顏色等),初始化view(backgroundView、bezelView、label、detailLabel、button、topSpacer、bottomSpacer),且會預設建立一個indicator。之後hud會顯示在螢幕上,但由於約束未觸發,因此使用者看不到。hud.mode = MBProgressHUDModeText
。HUD會根據mode的值去隱藏indicator,並更新約束。hud.label.text = NSLocalizedString(@"Message here!", @"HUD message title")
設定要顯示的文字。hud.offset = CGPointMake(0.f, MBProgressMaxOffset)
設定bezelView的偏移屬性:讓其顯示在最底部。並更新約束。[hud hideAnimated:YES afterDelay:3.f]
設定一個延遲timer,在3s之後隱藏hud。隱藏之後呼叫completionBlock
和代理方法- (void)hudWasHidden:(MBProgressHUD *)hud;
。
2.載入(菊花)
載入樣式表現為一個旋轉的菊花,底部也可包含"Loading…"等字樣提示。我們以包含"Loading…"字樣的HUD為例剖析其內部原理。程式碼如下:
- (void)labelExample {
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
// Set the label text.
hud.label.text = NSLocalizedString(@"Loading...", @"HUD loading title");
// You can also adjust other label properties if needed.
// hud.label.font = [UIFont italicSystemFontOfSize:16.f];
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
[self doSomeWork];
dispatch_async(dispatch_get_main_queue(), ^{
[hud hideAnimated:YES];
});
});
}
- 呼叫
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
建立HUD例項,過程跟純文字是一樣的。 hud.label.text = NSLocalizedString(@"Loading...", @"HUD loading title");
配置提示文案為"Loading"。- 之後在
global_queue
裡面執行任務,完成任務之後回到主執行緒隱藏HUD。
通過分析純文字HUD的建立過程我們知道,hud在初始化的時候,它的mode預設為MBProgressHUDModeIndeterminate
,也就是說單純的呼叫MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
建立出來的HUD就是帶有菊花載入控制元件的HUD,我們接下來做的就是給它的label賦上文案即可。
3.條狀進度條
MBProgressHUD提供了三種樣式的進度條:條狀、餅狀、環狀。其中餅狀和環狀差不多,接下來我們分析下條狀進度條的實現原理:
- (void)barDeterminateExample {
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
// Set the bar determinate mode to show task progress.
hud.mode = MBProgressHUDModeDeterminateHorizontalBar;
hud.label.text = NSLocalizedString(@"Loading...", @"HUD loading title");
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
// Do something useful in the background and update the HUD periodically.
[self doSomeWorkWithProgress];
dispatch_async(dispatch_get_main_queue(), ^{
[hud hideAnimated:YES];
});
});
}
- 呼叫
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
建立HUD例項,過程跟純文字是一樣的。 hud.mode = MBProgressHUDModeDeterminateHorizontalBar;
設定mode為條狀進度條。在mode的setter方法中會呼叫- (void)updateIndicator
建立進度條indicator。- 進度條indicator是類
MBBarProgressView
的例項。建立時預設寬為120,高為20,內容高度(intrinsicContentSize
)為10。它的樣式是在- (void)drawRect
中繪製的。在其progress
屬性的setter方法中呼叫了-(void)setNeedsDisplay
從而觸發- (void)drawRect
來更新進度。 hud.label.text = NSLocalizedString(@"Loading...", @"HUD loading title");
配置HUD提示文案為"Loading"。
4.自定義檢視
MBProgressHUD提供了顯示自定義檢視的功能。在Demo中是展示一個對勾。
- (void)customViewExample {
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
// Set the custom view mode to show any view.
hud.mode = MBProgressHUDModeCustomView;
// Set an image view with a checkmark.
UIImage *image = [[UIImage imageNamed:@"Checkmark"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
hud.customView = [[UIImageView alloc] initWithImage:image];
// Looks a bit nicer if we make it square.
hud.square = YES;
// Optional label text.
hud.label.text = NSLocalizedString(@"Done", @"HUD done title");
[hud hideAnimated:YES afterDelay:3.f];
}
- 呼叫
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
建立HUD例項,過程跟純文字是一樣的。 hud.mode = MBProgressHUDModeCustomView;
設定mode為自定義檢視。接下來將需要展示的自定義檢視賦值給hud的customView
屬性。在customView
屬性的setter方法中會呼叫- (void)updateIndicators
將customView
新增到HUD上。- 為了讓介面美觀,規定hud顯示為方形:
hud.square = YES;
。 hud.label.text = NSLocalizedString(@"Loading...", @"HUD loading title");
配置HUD提示文案為"Loading"。
至此,我們已經簡單瞭解了MBProgressHUD的整個程式碼結構及使用流程,這已經足夠我們去建立和使用符合我們需求的HUD了。但其實MBProgressHUD的原始碼中還包含不少高階的技術細節,我們將在下篇文章中一個個分析學習。