Showing the HUD(显示弹框)
从这篇文章开始我们开始分析另一个常用的框架SVProgressHUD,这个框架类似于MBProgressHUD效果,可以出现一个模态框。
使用场景:登陆界面,加载界面等多种与用户交互的界面
效果图如下
基本show显示
+ (void)show;
+ (void)showWithStatus:(NSString*)string;
先来看最简单的show类方法
+ (void)show {
/* 调用自己的显示状态方法,什么也不显示(文字) */
[self showWithStatus:nil];
}
+ (void)showWithStatus:(NSString*)status {
[self showProgress:SVProgressHUDUndefinedProgress status:status];
}
#pragma mark - Master show/dismiss methods
- (void)showProgress:(float)progress status:(NSString*)status {
/* weakSelf之所以设置成为__weak为了防止出现block循环引用,因此需要甚至成为弱变量 */
__weak SVProgressHUD *weakSelf = self;
/**
* 更改UI在主线程
* 为什么这样设计???
* 外部可能在多线程下调用,更新UI必须在主线程。因此,需要回归主线程,内部就帮助开发人员回归主线程
* 外部可以调用,但建议外部放到主线程去调用,不然对导致,控制台出现僵尸现象
*/
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
__strong SVProgressHUD *strongSelf = weakSelf;
if(strongSelf){
if(strongSelf.fadeOutTimer) {
strongSelf.activityCount = 0;
}
// Stop timer
strongSelf.fadeOutTimer = nil;
strongSelf.graceTimer = nil;
// Update / Check view hierarchy to ensure the HUD is visible
/* 更新或者检查HUD能够正常显示 */
[strongSelf updateViewHierarchy];
// Reset imageView and fadeout timer if an image is currently displayed
// 图片加载部分设置为隐藏
strongSelf.imageView.hidden = YES;
// 图片设置为空
strongSelf.imageView.image = nil;
// Update text and set progress to the given value
// 设置文字长度为空
strongSelf.statusLabel.hidden = status.length == 0;
//文字内容为传入的文字
strongSelf.statusLabel.text = status;
strongSelf.progress = progress;
// Choose the "right" indicator depending on the progress
if(progress >= 0) {
// Cancel the indefiniteAnimatedView, then show the ringLayer
// 先取消动画
[strongSelf cancelIndefiniteAnimatedViewAnimation];
// Add ring to HUD
if(!strongSelf.ringView.superview){
[strongSelf.hudView.contentView addSubview:strongSelf.ringView];
}
if(!strongSelf.backgroundRingView.superview){
[strongSelf.hudView.contentView addSubview:strongSelf.backgroundRingView];
}
// Set progress animated
// 设置动画效果
[CATransaction begin];
[CATransaction setDisableActions:YES];
strongSelf.ringView.strokeEnd = progress;
[CATransaction commit];
// Update the activity count
if(progress == 0) {
strongSelf.activityCount++;
}
} else {
// Cancel the ringLayer animation, then show the indefiniteAnimatedView
[strongSelf cancelRingLayerAnimation];
// Add indefiniteAnimatedView to HUD
[strongSelf.hudView.contentView addSubview:strongSelf.indefiniteAnimatedView];
if([strongSelf.indefiniteAnimatedView respondsToSelector:@selector(startAnimating)]) {
[(id)strongSelf.indefiniteAnimatedView startAnimating];
}
// Update the activity count
strongSelf.activityCount++;
}
// Fade in delayed if a grace time is set
if (self.graceTimeInterval > 0.0 && self.backgroundView.alpha == 0.0f) {
strongSelf.graceTimer = [NSTimer timerWithTimeInterval:self.graceTimeInterval target:strongSelf selector:@selector(fadeIn:) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:strongSelf.graceTimer forMode:NSRunLoopCommonModes];
} else {
[strongSelf fadeIn:nil];
}
// Tell the Haptics Generator to prepare for feedback, which may come soon
#if TARGET_OS_IOS && __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000
if (@available(iOS 10.0, *)) {
[strongSelf.hapticGenerator prepare];
}
#endif
}
}];
}
上面就是源码给的定义,我们逐一分析:
第一个模块:内部实际上调用了含有文字内容的方法
[self showWithStatus:nil];
第二个模块:在一次进行封装,传入类型是一个枚举法,表明显示的类型和传入的文字内容
[self showProgress:SVProgressHUDUndefinedProgress status:status];
第三模块:核心代码分析:
- 防止block循环操作
__weak SVProgressHUD *weakSelf = self;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
__strong SVProgressHUD *strongSelf = weakSelf;
思考:为什么会出现循环引用问题,以及如何防止循环引用?
个人理解,当传入block的self进入block前是强引用(self自身就是一个强引用),在进入block后依旧强引用,从而导致block中强引用无法被释放,也就是一直在强引用block,从而导致发生了block循环引用(强强联手),解决办法,在进入block前将strong类型的self转换为weak类型,再在内部转换为strong类型,这样就可以在内部使用self对应的操作,但是此时的block用完就会被释放掉,从而防止了循环引用的问题。
从上面我们看到,添加操作的线程是放在了主线程当中,为什么这么做?因为,更新UI时候,必须在主线程中完成,假设,现在在做网络请求,为了防止阻塞线程,开发者放到了一个自定义线程中(非主线程),那么此时就需要回归主线程从而更新UI,那么SVProgressHUD这样设计目的就是防止程序员未回归主线程而导致程序无法正常运行。
// Stop timer
strongSelf.fadeOutTimer = nil;
strongSelf.graceTimer = nil;
// Update / Check view hierarchy to ensure the HUD is visible
/* 更新或者检查HUD能够正常显示 */
[strongSelf updateViewHierarchy];
// Reset imageView and fadeout timer if an image is currently displayed
// 图片加载部分设置为隐藏
strongSelf.imageView.hidden = YES;
// 图片设置为空
strongSelf.imageView.image = nil;
// Update text and set progress to the given value
// 设置文字长度为空
strongSelf.statusLabel.hidden = status.length == 0;
//文字内容为传入的文字
strongSelf.statusLabel.text = status;
strongSelf.progress = progress;
这里面进行了一些个性化操作,视图的消失时间,图片显示,文字的显示和文字的内容以及进度条的显示。
这里面我们进行了操作更新视图层
updateViewHierarchy
- (void)updateViewHierarchy {
// Add the overlay to the application window if necessary
if(!self.controlView.superview) {
//if nil then use default window level -- 当为空时候和窗口的级别相同
if(self.containerView){
//如果没有当前的父类视图,就判断父类是否存在,存在添加
[self.containerView addSubview:self.controlView];
} else {
//不存在,和window一个级别
#if !defined(SV_APP_EXTENSIONS)
[self.frontWindow addSubview:self.controlView];
#else
// If SVProgressHUD is used inside an app extension add it to the given view
if(self.viewForExtension) {
[self.viewForExtension addSubview:self.controlView];
}
#endif
}
} else {
// The HUD is already on screen, but maybe not in front. Therefore
// ensure that overlay will be on top of rootViewController (which may
// be changed during runtime).
// 如果存在要显示的窗口,就将窗口显示在最外层
// bringSubviewToFront -- 系统的方法,会自动把视图添加到最外层
// HUD已显示在屏幕上,但可能不在前面。因此,请确保叠加层将位于rootViewController的顶部
[self.controlView.superview bringSubviewToFront:self.controlView];
}
// Add self to the overlay view
if(!self.superview) {
[self.controlView addSubview:self];
}
}
这里面判断是否当前窗口的父窗口为空,如果为空则添加,如果存在就将HUD显示在屏幕的最前方
[self.controlView.superview bringSubviewToFront:self.controlView];
bringSubviewToFront : 将视图添加在最前面,从而达到模态框到效果。
这里面官方定义传入的进度值为-1
static const CGFloat SVProgressHUDUndefinedProgress = -1;
因此,我们只看else中的操作
// Cancel the ringLayer animation, then show the indefiniteAnimatedView
[strongSelf cancelRingLayerAnimation];
// Add indefiniteAnimatedView to HUD
[strongSelf.hudView.contentView addSubview:strongSelf.indefiniteAnimatedView];
if([strongSelf.indefiniteAnimatedView respondsToSelector:@selector(startAnimating)]) {
[(id)strongSelf.indefiniteAnimatedView startAnimating];
}
// Update the activity count
strongSelf.activityCount++;
首先,如果有HUD动画再显示,我们应该先取消动画然后在重新加载动画效果。然后更新活动的数目,在后面我们会知道为什么设计活动数目了。
如果设置了宽限时间,淡入将延迟图像将自动关闭。因此将持续时间作为userInfo传递。
设置了延迟效果的时间,如果有设计了延迟时间,则延迟显示
if (self.graceTimeInterval > 0.0 && self.backgroundView.alpha == 0.0f) {
strongSelf.graceTimer = [NSTimer timerWithTimeInterval:self.graceTimeInterval target:strongSelf selector:@selector(fadeIn:) userInfo:@(duration) repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:strongSelf.graceTimer forMode:NSRunLoopCommonModes];
} else {
[strongSelf fadeIn:@(duration)];
}
这里面我们需要关心几个问题,到目前为止,我们还没有定义基本的HUD样式和动画显示。
HUD样式
这一部分做了样式设计,我们看看具体怎么实现的。
[strongSelf.hudView.contentView addSubview:strongSelf.indefiniteAnimatedView];
- (UIView*)indefiniteAnimatedView {
// Get the correct spinner for defaultAnimationType
// 判断动画的效果
if(self.defaultAnimationType == SVProgressHUDAnimationTypeFlat){
// 默认的动画类型
// Check if spinner exists and is an object of different class
if(_indefiniteAnimatedView && ![_indefiniteAnimatedView isKindOfClass:[SVIndefiniteAnimatedView class]]){
[_indefiniteAnimatedView removeFromSuperview];
_indefiniteAnimatedView = nil;
}
if(!_indefiniteAnimatedView){
_indefiniteAnimatedView = [[SVIndefiniteAnimatedView alloc] initWithFrame:CGRectZero];
}
// Update styling
SVIndefiniteAnimatedView *indefiniteAnimatedView = (SVIndefiniteAnimatedView*)_indefiniteAnimatedView;
indefiniteAnimatedView.strokeColor = self.foregroundImageColorForStyle;
indefiniteAnimatedView.strokeThickness = self.ringThickness;
indefiniteAnimatedView.radius = self.statusLabel.text ? self.ringRadius : self.ringNoTextRadius;
} else {
// Check if spinner exists and is an object of different class
if(_indefiniteAnimatedView && ![_indefiniteAnimatedView isKindOfClass:[UIActivityIndicatorView class]]){
[_indefiniteAnimatedView removeFromSuperview];
_indefiniteAnimatedView = nil;
}
if(!_indefiniteAnimatedView){
_indefiniteAnimatedView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
}
// Update styling
UIActivityIndicatorView *activityIndicatorView = (UIActivityIndicatorView*)_indefiniteAnimatedView;
activityIndicatorView.color = self.foregroundImageColorForStyle;
}
[_indefiniteAnimatedView sizeToFit];
return _indefiniteAnimatedView;
}
@property (assign, nonatomic) SVProgressHUDAnimationType defaultAnimationType UI_APPEARANCE_SELECTOR; // default is SVProgressHUDAnimationTypeFlat
首先,系统默认的是SVProgressHUDAnimationTypeFlat,我们只看if操作。
如果定义了动画视图,并且动画视图和需要的类型不相符合时候,先将定义好的动画视图移除父视图,并置空。
如果不存在,就要初始化设置,有趣的是大小设置为CGRectZero,等同于大小没有。
SVIndefiniteAnimatedView *indefiniteAnimatedView = (SVIndefiniteAnimatedView*)_indefiniteAnimatedView;
indefiniteAnimatedView.strokeColor = self.foregroundImageColorForStyle;
indefiniteAnimatedView.strokeThickness = self.ringThickness;
indefiniteAnimatedView.radius = self.statusLabel.text ? self.ringRadius : self.ringNoTextRadius;
剩下的设置了一下样式
- 边框颜色:前景图片样式;
- 边框样式:环形样式;
- 圆环:根据文本有无设计不同的圆环(大小不一样);
最后为了适应不同的手机屏幕,采用sizeToFit。所以,前面使用自动适应操作。
@property (assign, nonatomic) CGFloat ringRadius UI_APPEARANCE_SELECTOR; // default is 18 pt
@property (assign, nonatomic) CGFloat ringNoTextRadius UI_APPEARANCE_SELECTOR; // default is 24 pt
fadeIn 动画显示
- (void)fadeIn:(id)data {
// Update the HUDs frame to the new content and position HUD
[self updateHUDFrame];
[self positionHUD:nil];
// Update accessibility as well as user interaction
// \n cause to read text twice so remove "\n" new line character before setting up accessiblity label
NSString *accessibilityString = [[self.statusLabel.text componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]] componentsJoinedByString:@" "];
if(self.defaultMaskType != SVProgressHUDMaskTypeNone) {
self.controlView.userInteractionEnabled = YES;
self.accessibilityLabel = accessibilityString ?: NSLocalizedString(@"Loading", nil);
self.isAccessibilityElement = YES;
self.controlView.accessibilityViewIsModal = YES;
} else {
self.controlView.userInteractionEnabled = NO;
self.hudView.accessibilityLabel = accessibilityString ?: NSLocalizedString(@"Loading", nil);
self.hudView.isAccessibilityElement = YES;
self.controlView.accessibilityViewIsModal = NO;
}
// Get duration
id duration = [data isKindOfClass:[NSTimer class]] ? ((NSTimer *)data).userInfo : data;
// Show if not already visible
if(self.backgroundView.alpha != 1.0f) {
// Post notification to inform user
[[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDWillAppearNotification
object:self
userInfo:[self notificationUserInfo]];
// Shrink HUD to to make a nice appear / pop up animation
self.hudView.transform = self.hudView.transform = CGAffineTransformScale(self.hudView.transform, 1/1.5f, 1/1.5f);
__block void (^animationsBlock)(void) = ^{
// Zoom HUD a little to make a nice appear / pop up animation
self.hudView.transform = CGAffineTransformIdentity;
// Fade in all effects (colors, blur, etc.)
[self fadeInEffects];
};
__block void (^completionBlock)(void) = ^{
// Check if we really achieved to show the HUD (<=> alpha)
// and the change of these values has not been cancelled in between e.g. due to a dismissal
if(self.backgroundView.alpha == 1.0f){
// Register observer <=> we now have to handle orientation changes etc.
[self registerNotifications];
// Post notification to inform user
[[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDDidAppearNotification
object:self
userInfo:[self notificationUserInfo]];
// Update accessibility
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, self.statusLabel.text);
// Dismiss automatically if a duration was passed as userInfo. We start a timer
// which then will call dismiss after the predefined duration
if(duration){
self.fadeOutTimer = [NSTimer timerWithTimeInterval:[(NSNumber *)duration doubleValue] target:self selector:@selector(dismiss) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:self.fadeOutTimer forMode:NSRunLoopCommonModes];
}
}
};
// Animate appearance
if (self.fadeInAnimationDuration > 0) {
// Animate appearance
[UIView animateWithDuration:self.fadeInAnimationDuration
delay:0
options:(UIViewAnimationOptions) (UIViewAnimationOptionAllowUserInteraction | UIViewAnimationCurveEaseIn | UIViewAnimationOptionBeginFromCurrentState)
animations:^{
animationsBlock();
} completion:^(BOOL finished) {
completionBlock();
}];
} else {
animationsBlock();
completionBlock();
}
// Inform iOS to redraw the view hierarchy
[self setNeedsDisplay];
} else {
// Update accessibility
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, self.statusLabel.text);
// Dismiss automatically if a duration was passed as userInfo. We start a timer
// which then will call dismiss after the predefined duration
if(duration){
self.fadeOutTimer = [NSTimer timerWithTimeInterval:[(NSNumber *)duration doubleValue] target:self selector:@selector(dismiss) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:self.fadeOutTimer forMode:NSRunLoopCommonModes];
}
}
}
看到这里,你一定很崩溃,这么长,该从何下手,不要担心,秉持着if-else只看一半的操作,代码可以大大简化。
- 更新大小和位置
[self updateHUDFrame];
[self positionHUD:nil];
- 文本显示的处理 – 清除了换行操作
NSString *accessibilityString = [[self.statusLabel.text componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]] componentsJoinedByString:@" "];
- 默认的遮罩类型
@property (assign, nonatomic) SVProgressHUDMaskType defaultMaskType UI_APPEARANCE_SELECTOR; // default is SVProgressHUDMaskTypeNone
默认是SVProgressHUDMaskTypeNone,因此我们只看if部分
这里面做了一些基础设置,
1. 用户交互操作为真;
2. 显示的文本内容,如果有用户定义的文本内容则显示用户定义的内容,否则显示为空;
3. 设置其余选项设置
self.controlView.userInteractionEnabled = YES;
self.accessibilityLabel = accessibilityString ?: NSLocalizedString(@"Loading", nil);
self.isAccessibilityElement = YES;
self.controlView.accessibilityViewIsModal = YES;
- 动画时间设置
id duration = [data isKindOfClass:[NSTimer class]] ? ((NSTimer *)data).userInfo : data
- 透明度显示
@property(nonatomic) CGFloat alpha; // animatable. default is 1.0
默认定义的大小为1.0,不透明。因此,我们只看else部分操作。
// Update accessibility
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, self.statusLabel.text);
// Dismiss automatically if a duration was passed as userInfo. We start a timer
// which then will call dismiss after the predefined duration
if(duration){
self.fadeOutTimer = [NSTimer timerWithTimeInterval:[(NSNumber *)duration doubleValue] target:self selector:@selector(dismiss) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:self.fadeOutTimer forMode:NSRunLoopCommonModes];
}
做了一个显示的准备操作和取消的基本操作。当duration时间到了后,就调用dismiss取消显示。这一部分下一篇文章详细解析dismiss。
至此,基本显示操作就算完成了,在这里我们需要了解,这个框架多用枚举以及封装,并且对于每一种情况做出了不同的分析,因此,阅读时候,先从当前正常运行的路径来分析即可。
中文源码分析下载
源码下载: GitHub.