MBProgressHUD 源码笔记

MBProgressHUD 是开发中使用非常频繁的一个提示框的第三方库,MBProgressHUD 用法简单、源代码也比较少,它的使用方法可以参考Github 上的说明,网络上也有很多对 MBProgressHUD 的源码解析,但是别人的解析终归是别人的,我们只能作为参考,作为学习还是需要亲自去源码里一探究竟的,下面就记录一下我对 MBProgressHUD 框架的源码的学习记录,本文使用的源代码版本为:1.0.0

核心方法和属性

核心方法

类方法

1
2
3
4
5
6
7
8
//创建一个HUD并且添加到view上
+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated;
//隐藏view上的HUD
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated;
//找到view最顶层的HUD并返回,上面的方法内部会调用这个方法拿到HUD
+ (nullable MBProgressHUD *)HUDForView:(UIView *)view;

实例方法

1
2
3
4
5
6
7
8
9
10
11
//便利构造方法,实例一个和 View 一样大小的HUD
- (instancetype)initWithView:(UIView *)view;
//显示HUD
- (void)showAnimated:(BOOL)animated;
//隐藏HUD
- (void)hideAnimated:(BOOL)animated;
//延迟指定时间后隐藏HUD
- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay;

重要属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//HUD 隐藏之后是否从父控件移除 默认 NO
@property (assign, nonatomic) BOOL removeFromSuperViewOnHide;
//HUD 指示器显示的类型模式 默认 MBProgressHUDModeIndeterminate
@property (assign, nonatomic) MBProgressHUDMode mode;
//HUD 显示或者隐藏时的动画类型
@property (assign, nonatomic) MBProgressHUDAnimation animationType
//HUD 显示的最小时间 默认 0
@property (assign, nonatomic) NSTimeInterval minShowTime;
//HUD从 show 开始执行到显示HUD的时间,默认为0
@property (assign, nonatomic) NSTimeInterval graceTime;
/// bezelView 是指包括文本和指示器的视图
@property (strong, nonatomic, readonly) MBBackgroundView *bezelView;
/// backgroundView 背景视图
@property (strong, nonatomic, readonly) MBBackgroundView *backgroundView;
/// customView 自定义视图
@property (strong, nonatomic, nullable) UIView *customView;
/// 提示文本Label
@property (strong, nonatomic, readonly) UILabel *label;
/// 提示详情文本Label
@property (strong, nonatomic, readonly) UILabel *detailsLabel;
/// HUD 上面的button,可以添加事件,但是如果不添加时间,该button 不会显示
@property (strong, nonatomic, readonly) UIButton *button;

内部具体实现

接下来一步步看 HUD 内部是如何实现并调用的

构造方法

首先是 MBProgressHUD 最主要的一个类方法:

1
2
3
4
5
6
7
+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated {
MBProgressHUD *hud = [[self alloc] initWithView:view];
hud.removeFromSuperViewOnHide = YES;
[view addSubview:hud];
[hud showAnimated:animated];
return hud;
}

这里面先是使用一个便利构造函数 - (id)initWithView:(UIView *)view 创建了一个 HUD 实例对象,然后再调用了 - (void)showAnimated:(BOOL)animated 显示 HUD

不管是便利构造方法还是系统的构造方法最终都会来到 - (void)commonInit 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
- (instancetype)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
[self commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if ((self = [super initWithCoder:aDecoder])) {
[self commonInit];
}
return self;
}
- (id)initWithView:(UIView *)view {
NSAssert(view, @"View must not be nil.");
return [self initWithFrame:view.bounds];
}
- (void)commonInit {
// Set default values for properties
_animationType = MBProgressHUDAnimationFade;
_mode = MBProgressHUDModeIndeterminate;
_margin = 20.0f;
_opacity = 1.f;
_defaultMotionEffectsEnabled = YES;
// Default color, depending on the current iOS version
BOOL isLegacy = kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iOS_7_0;
_contentColor = isLegacy ? [UIColor whiteColor] : [UIColor colorWithWhite:0.f alpha:0.7f];
// Transparent background
self.opaque = NO;
self.backgroundColor = [UIColor clearColor];
// Make it invisible for now
self.alpha = 0.0f;
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.layer.allowsGroupOpacity = NO;
//设置子View
[self setupViews];
//设置指示器
[self updateIndicators];
//注册通知
[self registerForNotifications];
}

- (void)commonInit 方法里面设置了一些默认的属性,这里面又调用了 - (void)setupViews- updateIndicators- registerForNotifications 这三个方法

先来看一下 - (void)setupViews 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
- (void)setupViews {
UIColor *defaultColor = self.contentColor;
//背景view
MBBackgroundView *backgroundView = [[MBBackgroundView alloc] initWithFrame:self.bounds];
backgroundView.style = MBProgressHUDBackgroundStyleSolidColor;
backgroundView.backgroundColor = [UIColor clearColor];
backgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
backgroundView.alpha = 0.f;
[self addSubview:backgroundView];
_backgroundView = backgroundView;
//放指示器的view
MBBackgroundView *bezelView = [MBBackgroundView new];
bezelView.translatesAutoresizingMaskIntoConstraints = NO;
bezelView.layer.cornerRadius = 5.f;
bezelView.alpha = 0.f;
[self addSubview:bezelView];
_bezelView = bezelView;
[self updateBezelMotionEffects];
UILabel *label = [UILabel new];
label.adjustsFontSizeToFitWidth = NO;
label.textAlignment = NSTextAlignmentCenter;
label.textColor = defaultColor;
label.font = [UIFont boldSystemFontOfSize:MBDefaultLabelFontSize];
label.opaque = NO;
label.backgroundColor = [UIColor clearColor];
_label = label;
UILabel *detailsLabel = [UILabel new];
detailsLabel.adjustsFontSizeToFitWidth = NO;
detailsLabel.textAlignment = NSTextAlignmentCenter;
detailsLabel.textColor = defaultColor;
detailsLabel.numberOfLines = 0;
detailsLabel.font = [UIFont boldSystemFontOfSize:MBDefaultDetailsLabelFontSize];
detailsLabel.opaque = NO;
detailsLabel.backgroundColor = [UIColor clearColor];
_detailsLabel = detailsLabel;
UIButton *button = [MBProgressHUDRoundedButton buttonWithType:UIButtonTypeCustom];
button.titleLabel.textAlignment = NSTextAlignmentCenter;
button.titleLabel.font = [UIFont boldSystemFontOfSize:MBDefaultDetailsLabelFontSize];
[button setTitleColor:defaultColor forState:UIControlStateNormal];
_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];
}
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;
}

这个方法里面创建了HUD的全部控件,包括 backgroundViewbezelViewlabeldetailsLabelbutton 这几个控件,最后还创建了顶部视图和底部视图,不过默认是隐藏的,有一点需要注意,buttonMBProgressHUDRoundedButton 类型,它是 UIbutton 的子类,这个类里面有一个方法 - (CGSize)intrinsicContentSize,通过这个方法可以知道,button 如果没有事件响应,size 就是 CGSizeZero 也就不会显示

1
2
3
4
5
6
7
8
- (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;
}

- (void)updateIndicators 这个方法是设置HUD上的指示器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
- (void)updateIndicators {
UIView *indicator = self.indicator;
//判断当前指示器是否是 UIActivityIndicatorView
BOOL isActivityIndicator = [indicator isKindOfClass:[UIActivityIndicatorView class]];
//判断当前指示器是否是 MBRoundProgressView
BOOL isRoundIndicator = [indicator isKindOfClass:[MBRoundProgressView class]];
MBProgressHUDMode mode = self.mode;
if (mode == MBProgressHUDModeIndeterminate) {
if (!isActivityIndicator) {
// Update to indeterminate indicator
[indicator removeFromSuperview];
indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
[(UIActivityIndicatorView *)indicator startAnimating];
[self.bezelView addSubview:indicator];
}
}
else if (mode == MBProgressHUDModeDeterminateHorizontalBar) {
// Update to bar determinate indicator
[indicator removeFromSuperview];
indicator = [[MBBarProgressView alloc] init];
[self.bezelView addSubview:indicator];
}
else if (mode == MBProgressHUDModeDeterminate || mode == MBProgressHUDModeAnnularDeterminate) {
if (!isRoundIndicator) {
// Update to determinante indicator
[indicator removeFromSuperview];
indicator = [[MBRoundProgressView alloc] init];
[self.bezelView addSubview:indicator];
}
if (mode == MBProgressHUDModeAnnularDeterminate) {
[(MBRoundProgressView *)indicator setAnnular:YES];
}
}
else if (mode == MBProgressHUDModeCustomView && self.customView != indicator) {
// Update custom view indicator
[indicator removeFromSuperview];
indicator = self.customView;
[self.bezelView addSubview:indicator];
}
else if (mode == MBProgressHUDModeText) {
[indicator removeFromSuperview];
indicator = nil;
}
indicator.translatesAutoresizingMaskIntoConstraints = NO;
self.indicator = indicator;
//设置进度条的数值
if ([indicator respondsToSelector:@selector(setProgress:)]) {
[(id)indicator setValue:@(self.progress) forKey:@"progress"];
}
[indicator setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisHorizontal];
[indicator setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisVertical];
//更新控件颜色
[self updateViewsForColor:self.contentColor];
// 调用系统方法,更新控件约束
[self setNeedsUpdateConstraints];
}

指示器样式会根据 self.mode 去设置,并且添加到 bezelView,在 HUD 的 modecustomViewset 方法都会调用到 - (void)updateIndicatorsmode一共有六种模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef NS_ENUM(NSInteger, MBProgressHUDMode) {
/// 默认模式, 系统自带的指示器
MBProgressHUDModeIndeterminate,
/// 圆形饼图
MBProgressHUDModeDeterminate,
/// 水平进度条
MBProgressHUDModeDeterminateHorizontalBar,
/// 圆环
MBProgressHUDModeAnnularDeterminate,
/// 自定义视图
MBProgressHUDModeCustomView,
/// 只显示文字
MBProgressHUDModeText
};

- (void)registerForNotifications 这个方法中就是注册通知,它的作用是通过系统通知UIApplicationDidChangeStatusBarOrientationNotification 来处理屏幕转屏事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)registerForNotifications {
#if !TARGET_OS_TV
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:@selector(statusBarOrientationDidChange:)
name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];
#endif
}
- (void)unregisterFromNotifications {
#if !TARGET_OS_TV
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc removeObserver:self name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];
#endif
}

HUD show 系列方法

介绍完实例方法,接下来介绍一下 HUD 的显示方法,先看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated {
MBProgressHUD *hud = [[self alloc] initWithView:view];
hud.removeFromSuperViewOnHide = YES;
[view addSubview:hud];
[hud showAnimated:animated];
return hud;
}
- (void)showAnimated:(BOOL)animated {
//NSAssert 断言,保证HUD 的显示在主线程中
MBMainThreadAssert();
//取消最小显示HUD时间的定时器
[self.minShowTimer invalidate];
self.useAnimation = animated;
self.finished = NO;
// If the grace time is set, postpone the HUD display
//如果设置了宽限时间graceTime
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];
}
}
//graceTimer定时器方法
- (void)handleGraceTimer:(NSTimer *)theTimer {
// Show the HUD only if the task is still running
if (!self.hasFinished) {
[self showUsingAnimation:self.useAnimation];
}
}
// HUD 显示最后都会来到这个方法里
- (void)showUsingAnimation:(BOOL)animated {
// 移除之前存在的动画
[self.bezelView.layer removeAllAnimations];
[self.backgroundView.layer removeAllAnimations];
// 取消隐藏延迟定时器
[self.hideDelayTimer invalidate];
self.showStarted = [NSDate date];
self.alpha = 1.f;
// Needed in case we hide and re-show with the same NSProgress object attached.
// 设置进度显示
[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;
}
}

所有的显示方法最终都会来到 - (void)showUsingAnimation:(BOOL)animated,这个方法里面会调用 - (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled 方法去设置 progressObject 进度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled {
//使用CADisplayLink,是因为因为 NSProgress 会变化很快,使用 KVO 会是线程卡死,这样就会在每次刷新进度的时候,重新绘制图形
if (enabled && self.progressObject) {
// Only create if not already active.
if (!self.progressObjectDisplayLink) {
self.progressObjectDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateProgressFromProgressObject)];
}
} else {
self.progressObjectDisplayLink = nil;
}
}
//将进度设置给 self.progress
- (void)updateProgressFromProgressObject {
self.progress = self.progressObject.fractionCompleted;
}

CADisplayLink 是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器,在应用中创建一个新的 CADisplayLink 对象,把它添加到一个 Runloop 中,并给它提供一个 targetselector 在屏幕刷新的时候调用,一旦 CADisplayLink 以特定的模式注册到 Runloop 之后,每当屏幕需要刷新,Runloop 就会向 CADisplayLink 指定的 target 发送一次指定的 selector 消息,CADisplayLink 类对应的 selector 就会被调用一次

progressObject 是一个 NSProgress 对象,它将进度设置给 self.progress,然后会传给指示器的 progress

1
2
3
4
5
6
7
8
9
10
- (void)setProgress:(float)progress {
if (progress != _progress) {
_progress = progress;
UIView *indicator = self.indicator;
//设置指示器的progress
if ([indicator respondsToSelector:@selector(setProgress:)]) {
[(id)indicator setValue:@(self.progress) forKey:@"progress"];
}
}
}

当指示器是 MBRoundProgressViewMBRoundProgressView的时候就会调用指示器 progressset 方法重新绘制指示器视图

1
2
3
4
5
6
- (void)setProgress:(float)progress {
if (progress != _progress) {
_progress = progress;
[self setNeedsDisplay];
}
}

HUD hide 系列方法

隐藏 HUD 有如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated {
MBProgressHUD *hud = [self HUDForView:view];
if (hud != nil) {
hud.removeFromSuperViewOnHide = YES;
[hud hideAnimated:animated];
return YES;
}
return NO;
}
- (void)hideAnimated:(BOOL)animated {
MBMainThreadAssert();
[self.graceTimer invalidate];
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) {
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)hideUsingAnimation:(BOOL)animated {
if (animated && self.showStarted) {
self.showStarted = nil;
[self animateIn:NO withType:self.animationType completion:^(BOOL finished) {
[self done];
}];
} else {
self.showStarted = nil;
self.bezelView.alpha = 0.f;
self.backgroundView.alpha = 1.f;
[self done];
}
}

HUD 的隐藏方法和显示方法是对应的,代码结构也是类似的,隐藏的系列方法最后会都会来到 - (void)hideUsingAnimation:(BOOL)animated 方法,在这里发现在 animated = YES的时候它和 - (void)showUsingAnimation:(BOOL)animated 都调用了 - (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
- (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion {
// Automatically determine the correct zoom animation type
if (type == MBProgressHUDAnimationZoom) {
type = animatingIn ? MBProgressHUDAnimationZoomIn : MBProgressHUDAnimationZoomOut;
}
//动画缩放比例
CGAffineTransform small = CGAffineTransformMakeScale(0.5f, 0.5f);
CGAffineTransform large = CGAffineTransformMakeScale(1.5f, 1.5f);
// 设置动画初始状态
UIView *bezelView = self.bezelView;
if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomIn) {
bezelView.transform = small;
} else if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomOut) {
bezelView.transform = large;
}
// 创建动画任务
dispatch_block_t animations = ^{
if (animatingIn) {
bezelView.transform = CGAffineTransformIdentity;
} 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;
};
// Spring animations 弹性动画只能在 iOS7 +
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 || TARGET_OS_TV
if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_7_0) {
//iOS7 + 使用 Spring animations 执行动画
[UIView animateWithDuration:0.3 delay:0. usingSpringWithDamping:1.f initialSpringVelocity:0.f options:UIViewAnimationOptionBeginFromCurrentState animations:animations completion:completion];
return;
}
#endif
[UIView animateWithDuration:0.3 delay:0. options:UIViewAnimationOptionBeginFromCurrentState animations:animations completion:completion];
}

HUD 隐藏之后会调用 - (void)done 方法做一些结尾的工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)done {
// 移除延迟隐藏的定时器
[self.hideDelayTimer invalidate];
[self setNSProgressDisplayLinkEnabled:NO];
if (self.hasFinished) {
self.alpha = 0.0f;
if (self.removeFromSuperViewOnHide) {
[self removeFromSuperview];
}
}
MBProgressHUDCompletionBlock completionBlock = self.completionBlock;
if (completionBlock) {
completionBlock();
}
//代理
id<MBProgressHUDDelegate> delegate = self.delegate;
if ([delegate respondsToSelector:@selector(hudWasHidden:)]) {
[delegate performSelector:@selector(hudWasHidden:) withObject:self];
}
}

如果 removeFromSuperViewOnHide 属性为 YES,则将 HUD 从父视图上移除,如果有 completionBlock 回调函数,则执行回调,如果实现了代理并实现了代理方法,则执行代理方法

以上就是对 MBProgressHUD 源码的一些简单的总结和认识,MBProgressHUD 代码很少,结构也十分清晰,作为源码解析入门还是很不错的

本人刚开始写博客,主要是为了给自己的知识点做一个笔记,方便自己以后查阅,如果能让别人有所启发也是荣幸之至!如有错误,欢迎指正!