MBProgressHUD设计技巧小启发

小结MBProgressHUD的设计

http://www.jianshu.com/p/485b8d75ccd4

http://ju.outofmemory.cn/entry/124817

1. 工厂模式

updateIndicators中根据mode属性生成不同的View. 这种方式,在写UI组件会经常用到。

typedef NS_ENUM(NSInteger, MBProgressHUDMode) {
    /** 转菊花 Progress is shown using an UIActivityIndicatorView. This is the default. */
    MBProgressHUDModeIndeterminate,
    /** 饼图 Progress is shown using a round, pie-chart like, progress view. */
    MBProgressHUDModeDeterminate,
    /** 水平方向的进度条 Progress is shown using a horizontal progress bar */
    MBProgressHUDModeDeterminateHorizontalBar,
    /** 圆环 Progress is shown using a ring-shaped progress view. */
    MBProgressHUDModeAnnularDeterminate,
    /** 自定义View Shows a custom view */
    MBProgressHUDModeCustomView,
    /** Shows only labels */
    MBProgressHUDModeText
};

3.View管理自己的生命周期

开发中经常看到别人为了维护偶尔出现的View的生命周期(初始化、show、hide),在父view里添加属性或实例变量来持有子View的指针。看着总觉繁琐难受。MBProgressHUD这样的菊花,在一个页面的可能会多次出现、消失,但是我又不想在父View中持有它。 它的几个show和hide的类方法就很小淸新:

show
+ (MB_INSTANCETYPE)showHUDAddedTo:(UIView *)view animated:(BOOL)animated {
    MBProgressHUD *hud = [[self alloc] initWithView:view];
    hud.removeFromSuperViewOnHide = YES;
    [view addSubview:hud];
    [hud show:animated];
    return MB_AUTORELEASE(hud);
}
+ (MB_INSTANCETYPE)HUDForView:(UIView *)view {
    NSEnumerator *subviewsEnum = [view.subviews reverseObjectEnumerator];
    for (UIView *subview in subviewsEnum) {
        if ([subview isKindOfClass:self]) {
            return (MBProgressHUD *)subview;
        }
    }
    return nil;
}

父view调用此类方法时传入self指针,MBProgressHUD实例化自己,并add到super view上。

hide
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated {
    MBProgressHUD *hud = [self HUDForView:view];
    if (hud != nil) {
        hud.removeFromSuperViewOnHide = YES;
        [hud hide:animated];
        return YES;
    }
    return NO;
}

+ (MB_INSTANCETYPE)HUDForView:(UIView *)view {
    NSEnumerator *subviewsEnum = [view.subviews reverseObjectEnumerator];
    for (UIView *subview in subviewsEnum) {
        if ([subview isKindOfClass:self]) {
            return (MBProgressHUD *)subview;
        }
    }
    return nil;
}

+ (NSUInteger)hideAllHUDsForView:(UIView *)view animated:(BOOL)animated {
    NSArray *huds = [MBProgressHUD allHUDsForView:view];
    for (MBProgressHUD *hud in huds) {
        hud.removeFromSuperViewOnHide = YES;
        [hud hide:animated];
    }
    return [huds count];
}

+ (NSArray *)allHUDsForView:(UIView *)view {
    NSMutableArray *huds = [NSMutableArray array];
    NSArray *subviews = view.subviews;
    for (UIView *aView in subviews) {
        if ([aView isKindOfClass:self]) {
            [huds addObject:aView];
        }
    }
    return [NSArray arrayWithArray:huds];
}

MBProgressHUD自己拿到父View的指针,从superView.subviews数组中找出菊花视图,隐藏。

这种设计很适合一些不常驻的View。比如页面弹框。再配合Block使用(Block使用可以学习Masonry),代码看起来妥妥的。比如:

/** 
 alert
*/
+ (instancetype)presentAlertInView:(UIView *)superView
                        withTitle:(NSString *)title
                     confirmTitle:(NSString *)confirmTitle
                      cancelTitle:(NSString *)cancelTitle
                     confirmBlock:(void (^)(id sender))confirmBlock
                      cancelBlock:(void (^)(id sender))cancelBlock;
                      
+ (void)hideAllAlertForView:(UIView *)superview;

又比如一个结果弹层:

// 事件回调
typedef void(^RYButtonActionBlock)(void);

// 定义一个数据模型
@interface RYResultViewParams : NSObject

@property (nonatomic , strong) NSString  *param;
// ...

@interface RYResultView : UIView

/**
 用Block传递参数, 事件回调
 */
+ (RYResultView *)showReusltViewWithParams:(void (^) (RYResultViewParams *))params_block
                         buttonActionBlock:(RYButtonActionBlock)action;

/**
 隐藏
 */
+ (NSInteger)hideAllResultViewFromView:(UIView *)superView;
@end

@implementation RYResultView

+ (RYResultView *)showReusltViewWithParams:(void (^)(RYResultViewParams *))params_block buttonActionBlock:(RYButtonActionBlock)action
{
    RYResultView *resultView = [[RYResultView alloc] initWithParams:params_block buttonActionBlock:action];
    // ...
    
    return resultView;
}

+ (NSInteger)hideAllResultViewFromView:(UIView *)superView
{
    // ...
}

- (RYResultView *)initWithParams:(void (^)(RYResultViewParams *))params_block buttonActionBlock:(RYButtonActionBlock)action
{
    self = [super init];
    
    // 获取外部传入的参数
    RYResultViewParams *params = [RYResultViewParams new];
    if (params_block) {
        params_block(params);
    }
    
    return self;
}

外部使用简洁明了:

    [RYResultView showReusltViewWithParams:^(RYResultViewParams *parmas) {
         parmas.param = @"....";
        //...
    } buttonActionBlock:^{
        //....
    }];

KVO

具体KVO的介绍,仔细看看objc的文章哦 https://objccn.io/issue-7-3/

通常我们有这样的需求,一个对象属性值发生变化,会自动触发一些逻辑。就是观察者模式啦。而Observer模式中使用Notification, 走全局NotificationCenter太重了,KVO可以一对象对一对象, 被观察的属性setter方法或者通过KVC方式改变值时,都会触发- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 方法。善加应用,能让代码更简洁淸新。

  1. 注册observer
  2. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 对应path的属性值发生变化,需要触发的逻辑
  3. 移除observer,否则我们我们的 app 会因为某些奇怪的原因崩溃。
    比如被观察的对象已经delloc了,但是observers还没反注册,有野指针问题.
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x7fde9ae08040 of class RYMultimediaRecordView was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x60800042cd00> (
<NSKeyValueObservance 0x608000450980: Observer: 0x7fde9ae08040, Key path: isVideoRecording, Options: <New: YES, Old: NO, Prior: NO> Context: 0x0, Property: 0x6080006500b0>
)'
// MBProgressHUD初始化时被调用
- (void)registerForKVO {
    for (NSString *keyPath in [self observableKeypaths]) {
        [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL];
    }
}

// dealloc 中调用
- (void)unregisterFromKVO {
    for (NSString *keyPath in [self observableKeypaths]) {
        [self removeObserver:self forKeyPath:keyPath];
    }
}

// 需要观察的属性
- (NSArray *)observableKeypaths {
    return [NSArray arrayWithObjects:@"mode", @"customView", @"labelText", @"labelFont", @"labelColor",
            @"detailsLabelText", @"detailsLabelFont", @"detailsLabelColor", @"progress", @"activityIndicatorColor", nil];
}

// UI 更新
- (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"] || [keyPath isEqualToString:@"customView"] ||
        [keyPath isEqualToString:@"activityIndicatorColor"]) {
        [self updateIndicators];
    } else if ([keyPath isEqualToString:@"labelText"]) {
        label.text = self.labelText;
    } else if ([keyPath isEqualToString:@"labelFont"]) {
        label.font = self.labelFont;
    } else if ([keyPath isEqualToString:@"labelColor"]) {
        label.textColor = self.labelColor;
    } else if ([keyPath isEqualToString:@"detailsLabelText"]) {
        detailsLabel.text = self.detailsLabelText;
    } else if ([keyPath isEqualToString:@"detailsLabelFont"]) {
        detailsLabel.font = self.detailsLabelFont;
    } else if ([keyPath isEqualToString:@"detailsLabelColor"]) {
        detailsLabel.textColor = self.detailsLabelColor;
    } else if ([keyPath isEqualToString:@"progress"]) {
        if ([indicator respondsToSelector:@selector(setProgress:)]) {
            [(id)indicator setValue:@(progress) forKey:@"progress"];
        }
        return;
    }
    [self setNeedsLayout];
    [self setNeedsDisplay];
}

比如MBProgressHUD的labelFont属性发生变化,会触发一些UI刷新。这里的使用还比较简单初级。至于KVO和Context、NSKeyValueObservingOptions、线程啊,还是要强烈推荐这文章https://objccn.io/issue-7-3/

推荐阅读更多精彩内容