前言
阅读优秀的开源项目是提高编程能力的有效手段,我们能够从中开拓思维、拓宽视野,学习到很多不同的设计思想以及最佳实践。阅读他人代码很重要,但动手仿写、练习却也是很有必要的,它能进一步加深我们对项目的理解,将这些东西内化为自己的知识和能力。然而真正做起来却很不容易,开源项目阅读起来还是比较困难,需要一些技术基础和耐心。
本系列将对一些著名的iOS开源类库进行深入阅读及分析,并仿写这些类库的基本实现,加深我们对底层实现的理解和认识,提升我们iOS开发的编程技能。
MBProcessHUD
MBProcessHUD是一个iOS上的提示框库,支持加载提示、进度框、文字提示等,使用简单,功能强大,还能够自定义显示内容,广泛应用于iOS app中。这是它的地址:https://github.com/jdg/MBProgressHUD
简单看一下界面效果:
实现原理
MBProcessHUD继承自UIView,实际上是一个覆盖全屏的半透明指示器组件。它由以下几个部分构成,分别是:Loading加载动画,标题栏,背景栏以及其它栏(如详情栏、按钮)。我们把MBProcessHUD添加到页面上,显示任务进度及提示信息,同时屏蔽用户交互操作。
MBProcessHUD的Loading加载动画来自系统类UIActivityIndicatorView
,在页面加载时,开启转圈动画,页面销毁时取消转圈动画。
MBProcessHUD根据加载内容动态布局,它通过计算需要显示的内容,动态调整页面元素的位置大小,放置到屏幕的中央,显示的内容可以由使用者指定。MBProcessHUD v1.0版之前是通过frame计算各个元素的位置,最新的版本采用了约束布局。
MBProcessHUD使用KVO监听一些属性值的变化,如labelText,model。这些属性被修改时,MBProcessHUD视图相应更新,传入新值。
仿写MBProcessHUD
我们模仿MBProcessHUD写一个简单的弹出框组件,以加深对它的理解。在这个demo中,我们不完全重写MBProcessHUD,只实现基本功能。
首先在demo中创建ZCJHUD,继承UIView。
在ZCJHUD头文件中,定义几种显示模式
typedef NS_ENUM(NSInteger, ZCJHUDMode) {
/** 转圈动画模式,默认值 */
ZCJHUDModeIndeterminate,
/** 只显示标题 */
ZCJHUDModeText
};
定义对外的接口,显示模式mode,标题内容labelText
@interface ZCJHUD : UIView
@property (nonatomic, assign) ZCJHUDMode mode;
@property (nonatomic, strong) NSString *labelText;
- (instancetype)initWithView:(UIView *)view;
- (void)show;
- (void)hide;
@end
自身初始化,设置组件默认属性,更新布局,注册kvo监视属性变化。
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
_mode = ZCJHUDModeIndeterminate;
_labelText = nil;
_size = CGSizeZero;
self.opaque = NO;
self.backgroundColor = [UIColor clearColor];
self.alpha = 0;
[self setupView];
[self updateIndicators];
[self registerForKVO];
}
return self;
}
初始化转圈动画,并添加到hud上,ZCJHUDModeIndeterminate模式才有这个动画
- (void)updateIndicators {
BOOL isActivityIndicator = [_indicator isKindOfClass:[UIActivityIndicatorView class]];
if (_mode == ZCJHUDModeIndeterminate) {
if (!isActivityIndicator) {
// Update to indeterminate indicator
[_indicator removeFromSuperview];
self.indicator = ([[UIActivityIndicatorView alloc]
initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]);
[(UIActivityIndicatorView *)_indicator startAnimating];
[self addSubview:_indicator];
}
} else if (_mode == ZCJHUDModeText) {
[_indicator removeFromSuperview];
self.indicator = nil;
}
}
两个主要方法,显示和隐藏hud
-(void)show {
self.alpha = 1;
}
-(void)hide {
self.alpha = 0;
[self removeFromSuperview];
}
这里使用了frame动态布局
- (void)layoutSubviews {
[super layoutSubviews];
// 覆盖整个视图,屏蔽交互操作
UIView *parent = self.superview;
if (parent) {
self.frame = parent.bounds;
}
CGRect bounds = self.bounds;
CGFloat maxWidth = bounds.size.width - 4 * kMargin;
CGSize totalSize = CGSizeZero;
CGRect indicatorF = _indicator.bounds;
indicatorF.size.width = MIN(indicatorF.size.width, maxWidth);
totalSize.width = MAX(totalSize.width, indicatorF.size.width);
totalSize.height += indicatorF.size.height;
CGSize labelSize = MB_TEXTSIZE(_label.text, _label.font);
labelSize.width = MIN(labelSize.width, maxWidth);
totalSize.width = MAX(totalSize.width, labelSize.width);
totalSize.height += labelSize.height;
if (labelSize.height > 0.f && indicatorF.size.height > 0.f) {
totalSize.height += kPadding;
}
totalSize.width += 2 * kMargin;
totalSize.height += 2 * kMargin;
// Position elements
CGFloat yPos = round(((bounds.size.height - totalSize.height) / 2)) + kMargin;
CGFloat xPos = 0;
indicatorF.origin.y = yPos;
indicatorF.origin.x = round((bounds.size.width - indicatorF.size.width) / 2) + xPos;
_indicator.frame = indicatorF;
yPos += indicatorF.size.height;
if (labelSize.height > 0.f && indicatorF.size.height > 0.f) {
yPos += kPadding;
}
CGRect labelF;
labelF.origin.y = yPos;
labelF.origin.x = round((bounds.size.width - labelSize.width) / 2) + xPos;
labelF.size = labelSize;
_label.frame = labelF;
_size = totalSize;
}
绘制背景框
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
UIGraphicsPushContext(context);
CGContextSetGrayFillColor(context, 0.0f, 0.8);
// Center HUD
CGRect allRect = self.bounds;
// Draw rounded HUD backgroud rect
CGRect boxRect = CGRectMake(round((allRect.size.width - _size.width) / 2),
round((allRect.size.height - _size.height) / 2) , _size.width, _size.height);
float radius = 10;
CGContextBeginPath(context);
CGContextMoveToPoint(context, CGRectGetMinX(boxRect) + radius, CGRectGetMinY(boxRect));
CGContextAddArc(context, CGRectGetMaxX(boxRect) - radius, CGRectGetMinY(boxRect) + radius, radius, 3 * (float)M_PI / 2, 0, 0);
CGContextAddArc(context, CGRectGetMaxX(boxRect) - radius, CGRectGetMaxY(boxRect) - radius, radius, 0, (float)M_PI / 2, 0);
CGContextAddArc(context, CGRectGetMinX(boxRect) + radius, CGRectGetMaxY(boxRect) - radius, radius, (float)M_PI / 2, (float)M_PI, 0);
CGContextAddArc(context, CGRectGetMinX(boxRect) + radius, CGRectGetMinY(boxRect) + radius, radius, (float)M_PI, 3 * (float)M_PI / 2, 0);
CGContextClosePath(context);
CGContextFillPath(context);
UIGraphicsPopContext();
}
kvo监控属性变化,使用者在修改属性时,触发页面刷新,赋上新值。注意在页面销毁时要取消kvo监控,否则程序会崩溃
#pragma mark - KVO
- (void)registerForKVO {
for (NSString *keyPath in [self observableKeypaths]) {
[self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL];
}
}
- (void)unregisterFromKVO {
for (NSString *keyPath in [self observableKeypaths]) {
[self removeObserver:self forKeyPath:keyPath];
}
}
- (NSArray *)observableKeypaths {
return [NSArray arrayWithObjects:@"mode", @"labelText", nil];
}
- (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"]) {
[self updateIndicators];
} else if ([keyPath isEqualToString:@"labelText"]) {
_label.text = self.labelText;
}
}
- (void)dealloc {
[self unregisterFromKVO];
}
最终效果如下图:
最后附上 demo的地址:https://github.com/superzcj/ZCJHUD
总结
MBProcessHUD还是比较简单的,都是一些常用的东西。
希望借助这篇文章,动手仿写一遍MBProcessHUD,能更深刻地理解和认识MBProcessHUD。