閱讀優秀的開源項目是提高開發人員技術水平的最佳方法,我們能夠從中開拓思維、拓寬視野,學習到很多設計思想以及最佳實踐。如果再動手仿寫、練習這些項目,進一步加深對項目的理解,將這些東西內化為自己的知識和能力。然而真正做起來卻很不容易,開源項目閱讀起來還是比較困難,需要一些技術基礎和耐心。
本系列將對一些著名的iOS開源類庫進行深入閱讀及分析,並練習仿寫這些類庫的基本實現,提升我們iOS開發的編程技能。
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寫一個簡單的彈出框組件,以加深對它的理解。在這個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。
文章轉自 潇潇潇的簡