iOS开发造轮子 | UIView及其子类的占位图

iOS开发造轮子 | UIView及其子类的占位图_第1张图片
iu

这是要封装的

iOS开发造轮子 | UIView及其子类的占位图_第2张图片

这里展示的仅仅是tableView的占位图。

封装原因

最近重构分类详情页的时候需要实现这个功能,关于这个view我已经封装过几次了,今天我试着回想当初的思路,竟然没什么印象了!对于自己封装过的东西没什么印象原因只有一种:不够简洁。

iOS开发造轮子 | UIView及其子类的占位图_第3张图片
此事无关代码,关乎优雅

然后我翻看了一下自己以前的写的关于封装这个view的,不得不说,挺搓的。。。(每次回头看曾经的代码,都不甚满意。。。)

曾经的问题

1.首先,在基类里实现这个功能就是变相的提升程序的耦合度:我的目的是给tableView添加一个功能,并且是纯粹的添加功能,不需要新类,所以这种情况应该用category。想想MJRefresh,使用多么简单方便,用过一次就难以忘记。

2.连逻辑都繁琐不堪

这是使用方法:

  // 展示无数据占位图
  [self.tableView showEmptyViewWithType:NoContentTypeNetwork];
  // 无数据占位图点击的回调
  self.tableView.noContentViewTapedBlock = ^{
      [SVProgressHUD showSuccessWithStatus:@"没网"];
  };

  // 移除无数据占位图
  [self.tableView removeEmptyView];

如此不堪的使用方法,甚至还要手动移除占位图。

优化

之前只是针对UITableView占位图的封装,这次面向UIView

曾经的问题就是现在要解决的问题,在这之前,先将思路理一理:

封装这个view:
1.至少需要一个参数type来表示是哪种类型的占位图:没网or空数据等等;
2.需要一个回调来处理用户点击重新加载按钮事件。
3.每次展示的占位图只可能有一个。

代码

这是基于UIView的category:

@interface UIView ()

/** 占位图 */
@property (nonatomic, strong) UIView *cq_placeholderView;

@end

@implementation UIView (PlaceholderView)

static void *strKey = &strKey;

- (UIView *)cq_placeholderView {
    return objc_getAssociatedObject(self, &strKey);
}

- (void)setCq_placeholderView:(UIView *)cq_placeholderView {
    objc_setAssociatedObject(self, &strKey, cq_placeholderView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

/**
 展示UIView及其子类的占位图
 
 @param type 占位图类型
 @param reloadBlock 重新加载回调的block
 */
- (void)cq_showPlaceholderViewWithType:(CQPlaceholderViewType)type reloadBlock:(void (^)())reloadBlock {
    // 如果是UIScrollView及其子类,占位图展示期间禁止scroll
    BOOL originalScrollEnabled = NO; // 原本的scrollEnabled
    if ([self isKindOfClass:[UIScrollView class]]) {
        UIScrollView *scrollView = (UIScrollView *)self;
        // 先记录原本的scrollEnabled
        originalScrollEnabled = scrollView.scrollEnabled;
        // 再将scrollEnabled设为NO
        scrollView.scrollEnabled = NO;
    }
    
    //------- 占位图 -------//
    if (self.cq_placeholderView) {
        [self.cq_placeholderView removeFromSuperview];
        self.cq_placeholderView = nil;
    }
    self.cq_placeholderView = [[UIView alloc] init];
    [self addSubview:self.cq_placeholderView];
    self.cq_placeholderView.backgroundColor = [UIColor whiteColor];
    [self.cq_placeholderView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.center.mas_equalTo(self);
        make.size.mas_equalTo(self);
    }];
    
    //------- 图标 -------//
    UIImageView *imageView = [[UIImageView alloc] init];
    [self.cq_placeholderView addSubview:imageView];
    [imageView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.centerX.mas_equalTo(imageView.superview);
        make.centerY.mas_equalTo(imageView.superview).mas_offset(-80);
        make.size.mas_equalTo(CGSizeMake(70, 70));
    }];
    
    //------- 描述 -------//
    UILabel *descLabel = [[UILabel alloc] init];
    [self.cq_placeholderView addSubview:descLabel];
    [descLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.centerX.mas_equalTo(descLabel.superview);
        make.top.mas_equalTo(imageView.mas_bottom).mas_offset(20);
        make.height.mas_equalTo(15);
    }];
    
    //------- 重新加载button -------//
    UIButton *reloadButton = [[UIButton alloc] init];
    [self.cq_placeholderView addSubview:reloadButton];
    [reloadButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    [reloadButton setTitle:@"重新加载" forState:UIControlStateNormal];
    reloadButton.layer.borderWidth = 1;
    reloadButton.layer.borderColor = [UIColor blackColor].CGColor;
    [[reloadButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
        // 执行block回调
        if (reloadBlock) {
            reloadBlock();
        }
        // 从父视图移除
        [self.cq_placeholderView removeFromSuperview];
        self.cq_placeholderView = nil;
        // 复原UIScrollView的scrollEnabled
        if ([self isKindOfClass:[UIScrollView class]]) {
            UIScrollView *scrollView = (UIScrollView *)self;
            scrollView.scrollEnabled = originalScrollEnabled;
        }
    }];
    [reloadButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.centerX.mas_equalTo(reloadButton.superview);
        make.top.mas_equalTo(descLabel.mas_bottom).mas_offset(20);
        make.size.mas_equalTo(CGSizeMake(120, 30));
    }];
    
    //------- 根据type设置不同UI -------//
    switch (type) {
        case CQPlaceholderViewTypeNoNetwork: // 网络不好
        {
            NSString *path = [[NSBundle mainBundle] pathForResource:@"无网" ofType:@"png"];
            imageView.image = [UIImage imageWithContentsOfFile:path];
            descLabel.text = @"网络异常";
        }
            break;
            
        case CQPlaceholderViewTypeNoGoods: // 没商品
        {
            NSString *path = [[NSBundle mainBundle] pathForResource:@"无商品" ofType:@"png"];
            imageView.image = [UIImage imageWithContentsOfFile:path];
            descLabel.text = @"一个商品都没有";
        }
            break;
            
        case CQPlaceholderViewTypeNoComment: // 没评论
        {
            NSString *path = [[NSBundle mainBundle] pathForResource:@"沙发" ofType:@"png"];
            imageView.image = [UIImage imageWithContentsOfFile:path];
            descLabel.text = @"抢沙发!";
        }
            break;
            
        default:
            break;
    }
}

@end

使用方法

需要展示占位图的时候直接调用方法:

[self.tableView cq_showPlaceholderViewWithType:CQPlaceholderViewTypeNoComment reloadBlock:^{
    // 按钮点击回调
    [SVProgressHUD showInfoWithStatus:@"重新加载按钮点击"];
}];

细节

1. 对于UIScrollView及其子类,占位图展示期间将它的scrollEnabled设置为NO。

iOS开发造轮子 | UIView及其子类的占位图_第4张图片
这肯定不是我们想要的效果.gif

但是改变scrollEnabled的时候又不能对原控件造成侵入性,怎么破?

  • 先记录最初的scrollEnabled
// 如果是UIScrollView及其子类,占位图展示期间禁止scroll
BOOL originalScrollEnabled = NO; // 原本的scrollEnabled
if ([self isKindOfClass:[UIScrollView class]]) {
    UIScrollView *scrollView = (UIScrollView *)self;
    // 先记录原本的scrollEnabled
    originalScrollEnabled = scrollView.scrollEnabled;
    // 再将scrollEnabled设为NO
    scrollView.scrollEnabled = NO;
}
  • 移除占位图的时候复原UIScrollView的scrollEnabled
// 复原UIScrollView的scrollEnabled
if ([self isKindOfClass:[UIScrollView class]]) {
    UIScrollView *scrollView = (UIScrollView *)self;
    scrollView.scrollEnabled = originalScrollEnabled;
}

2. 给系统类扩展方法或添加属性都要加上前缀,向SDWebImage学习。

// SDWebImage的方法
[imageView sd_setImageWithURL:nil completed:nil];

3. 图片的加载

不需要一直放在内存里的用imageWithContentsofFile:方法。

4. block

不需要写weakSelf:

[self.view cq_showPlaceholderViewWithType:CQPlaceholderViewTypeNoNetwork reloadBlock:^{
    [SVProgressHUD showSuccessWithStatus:@"有网了"];
    // 直接写self也不会导致内存泄漏
    self.view.backgroundColor = [UIColor redColor];
}];

这里的block是局部变量,跟masonry的block是同一个道理:

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

总结

只有不断的反思和总结才能造出更优雅的轮子。

点击获取demo


2017年10月2日更新

根据评论区Jacob_Liang同学的建议,优化了部分代码,修复了内存泄漏问题,最新代码已更新到GitHub。感谢Jacob_Liang同学及时指出问题。


2017年10月7日更新

根据评论区S_Criminal_7a03同学的建议,新增一个指定占位图frame的方法:

/**
 展示UIView及其子类的占位图,大小可以设置(本质是在这个view上添加一个自定义view)
 
 @param frame 占位图的frame
 @param type 占位图类型
 @param reloadBlock 重新加载按钮点击时的回调
 */
- (void)cq_showPlaceholderViewWithFrame:(CGRect)frame type:(CQPlaceholderViewType)type reloadBlock:(void (^)())reloadBlock;

代码已合并到GitHub,感谢S_Criminal_7a03同学的提议。


2017年10月12日更新

iPhone X的出现断了frame最后的退路,所以我决定使用纯自动布局了。

默认情况下占位图的约束

@weakify(self);
[self.view cq_showPlaceholderViewWithType:CQPlaceholderViewTypeNoNetwork reloadBlock:^{
    @strongify(self);
    [SVProgressHUD showSuccessWithStatus:@"重新加载按钮点击"];
    self.view.backgroundColor = [UIColor redColor];
}];
iOS开发造轮子 | UIView及其子类的占位图_第5张图片
默认旋转适配.gif

可重新设置占位图的约束

[self.tableView cq_showPlaceholderViewWithType:CQPlaceholderViewTypeNoComment reloadBlock:nil];
self.tableView.cq_placeholderView.backgroundColor = [UIColor redColor];
// 重新设置占位图约束
[self.tableView.cq_placeholderView mas_remakeConstraints:^(MASConstraintMaker *make) {
    make.left.right.mas_equalTo(self.view);
    make.top.mas_equalTo(self.tableView).mas_offset(50);
    make.bottom.mas_equalTo(self.view).mas_offset(-30);
}];
iOS开发造轮子 | UIView及其子类的占位图_第6张图片
自定义约束的旋转适配.gif

代码已更新到GitHub

你可能感兴趣的:(iOS开发造轮子 | UIView及其子类的占位图)