iOS开发造轮子 | 通用占位图

作者: Lol刀妹 | 来源:发表于2017-05-20 12:13 被阅读1573次
    灵魂歌手-泰妍

    实际运用场景:

    没网时的提示view,tableView或collectionView没内容时的展示view,以及其它特殊情况时展示的特定view。如:


    常见的几种情况

    我的目标:

    对以上几种情况的展示view做统一封装,将来做新APP时,我只需在这个轮子上稍加修改就可实现相应需求。

    对自己的要求:

    代码简洁规范,逻辑清晰,保证写出的代码将来任何接手的人可以轻松读懂。

    对轮子的要求:

    • 使用方便
    • 易于维护修改
    • 高内聚,低耦合(不要因为这个轮子的加入而影响之前的代码)

    思路:

    • 这是一个view,可以添加到tableView或collectionView上的view,当然,也可以添加到其他类型的view上。
    • 一般说来,在一个项目中,从UI的角度来看,这样的view有几种,但是,从结构的角度来看,它们又一样,比如说:正中间一个imageView、imageView下方一个label、label下方一个button。
    • 点击这个view或者这个view上的button,会执行相应回调方法。

    先贴出代码,稍后细讲

    .h文件

    #import <UIKit/UIKit.h>
    
    /** 占位图的类型 */
    typedef NS_ENUM(NSInteger, CQPlaceholderViewType) {
        /** 没网 */
        CQPlaceholderViewTypeNoNetwork = 1,
        /** 没订单 */
        CQPlaceholderViewTypeNoOrder,
        /** 没商品 */
        CQPlaceholderViewTypeNoGoods,
        /** 美丽的妹纸 */
        CQPlaceholderViewTypeBeautifulGirl
    };
    
    #pragma mark - @protocol
    
    @class CQPlaceholderView;
    @protocol CQPlaceholderViewDelegate <NSObject>
    
    /** 占位图的重新加载按钮点击时回调 */
    - (void)placeholderView:(CQPlaceholderView *)placeholderView
       reloadButtonDidClick:(UIButton *)sender;
    
    @end
    
    #pragma mark - @interface
    
    @interface CQPlaceholderView : UIView
    
    /** 占位图类型(只读) */
    @property (nonatomic,assign,readonly) CQPlaceholderViewType type;
    /** 占位图的代理方(只读) */
    @property (nonatomic,weak,readonly) id <CQPlaceholderViewDelegate> delegate;
    
    /**
     构造方法
     
     @param frame 占位图的frame
     @param type 占位图的类型
     @param delegate 占位图的代理方
     @return 指定frame、类型和代理方的占位图
     */
    - (instancetype)initWithFrame:(CGRect)frame
                             type:(CQPlaceholderViewType)type
                         delegate:(id)delegate;
    
    @end
    

    .m文件

    #import "CQPlaceholderView.h"
    
    @implementation CQPlaceholderView
    
    #pragma mark - 构造方法
    /**
     构造方法
     
     @param frame 占位图的frame
     @param type 占位图的类型
     @param delegate 占位图的代理方
     @return 指定frame、类型和代理方的占位图
     */
    - (instancetype)initWithFrame:(CGRect)frame type:(CQPlaceholderViewType)type delegate:(id)delegate{
        if (self = [super initWithFrame:frame]) {
            // 存值
            _type = type;
            _delegate = delegate;
            // UI搭建
            [self setUpUI];
        }
        return self;
    }
    
    #pragma mark - UI搭建
    /** UI搭建 */
    - (void)setUpUI{
        self.backgroundColor = [UIColor whiteColor];
        
        //------- 图片在正中间 -------//
        UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(self.frame.size.width / 2 - 50, self.frame.size.height / 2 - 50, 100, 100)];
        [self addSubview:imageView];
        
        //------- 说明label在图片下方 -------//
        UILabel *descLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, CGRectGetMaxY(imageView.frame) + 10, self.frame.size.width, 20)];
        [self addSubview:descLabel];
        descLabel.textAlignment = NSTextAlignmentCenter;
        
        //------- 按钮在说明label下方 -------//
        UIButton *reloadButton = [[UIButton alloc]initWithFrame:CGRectMake(self.frame.size.width / 2 - 60, CGRectGetMaxY(descLabel.frame) + 5, 120, 25)];
        [self addSubview:reloadButton];
        [reloadButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
        reloadButton.layer.borderColor = [UIColor blackColor].CGColor;
        reloadButton.layer.borderWidth = 1;
        [reloadButton addTarget:self action:@selector(reloadButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
        
        //------- 根据type创建不同样式的UI -------//
        switch (_type) {
            case CQPlaceholderViewTypeNoNetwork: // 没网
            {
                imageView.image = [UIImage imageNamed:@"网络异常"];
                descLabel.text = @"没网,不约";
                [reloadButton setTitle:@"点击重试" forState:UIControlStateNormal];
            }
                break;
                
            case CQPlaceholderViewTypeNoOrder: // 没订单
            {
                imageView.image = [UIImage imageNamed:@"订单无数据"];
                descLabel.text = @"暂无订单";
                [reloadButton setTitle:@"没有拉到" forState:UIControlStateNormal];
            }
                break;
                
            case CQPlaceholderViewTypeNoGoods: // 没商品
            {
                imageView.image = [UIImage imageNamed:@"没商品"];
                descLabel.text = @"红旗连锁你的好邻居";
                [reloadButton setTitle:@"buybuybuy" forState:UIControlStateNormal];
            }
                break;
                
            case CQPlaceholderViewTypeBeautifulGirl: // 妹纸
            {
                imageView.image = [UIImage imageNamed:@"妹纸"];
                descLabel.text = @"你会至少在此停留3秒钟";
                [reloadButton setTitle:@"不爱妹纸" forState:UIControlStateNormal];
            }
                break;
                
            default:
                break;
        }
    }
    
    #pragma mark - 重新加载按钮点击
    /** 重新加载按钮点击 */
    - (void)reloadButtonClicked:(UIButton *)sender{
        // 代理方执行方法
        if ([_delegate respondsToSelector:@selector(placeholderView:reloadButtonDidClick:)]) {
            [_delegate placeholderView:self reloadButtonDidClick:sender];
        }
        // 从父视图上移除
        [self removeFromSuperview];
    }
    
    @end
    

    详细说明

    1.关于构造方法的设计

    /**
     构造方法
     
     @param frame 占位图的frame
     @param type 占位图的类型
     @param delegate 占位图的代理方
     @return 指定frame、类型和代理方的占位图
     */
    - (instancetype)initWithFrame:(CGRect)frame
                             type:(CQPlaceholderViewType)type
                         delegate:(id)delegate;
    

    三个参数说明:

    • frame:决定占位图的大小和位置。之所以需要这个参数是因为:这个占位图可能和你的tableView一样大,也可能是全屏的。
    • type:占位图的类型。这个参数决定占位图的展示样式。
    • delegate:代理方。处理事件。

    2.为什么type和delegate要设置为只读?

    /** 占位图类型(只读) */
    @property (nonatomic,assign,readonly) CQPlaceholderViewType type;
    /** 占位图的代理方(只读) */
    @property (nonatomic,weak,readonly) id <CQPlaceholderViewDelegate> delegate;
    

    因为这两个属性需要暴露出去,但是我又不希望它们在外部被修改,我希望构造方法是决定这个占位图属性的唯一方法。还有就是:让在外部能直接修改的越少,意外也就越少。

    3.为什么我的这代理方法命名如此冗长?

    /** 占位图的重新加载按钮点击时回调 */
    - (void)placeholderView:(CQPlaceholderView *)placeholderView
       reloadButtonDidClick:(UIButton *)sender;
    

    可能有人会说,你这个代理方法不就是想要执行重新加载数据方法嘛,直接命名为reloadData不是多好的?反正曾经懵懂的我看别人的代码时是这样想当然的认为的,直到有一天我看了官方文档以及回想了一下系统给代理方法的命名。
    其实代理方法的命名是一个非常讲究的东西,我之所以这样命名是完全参照官方文档的命名规范的,建议有疑问的瞅两眼delegate 命名。你也可以这样理解:代理方法,它描述的是某一个事件,而不是事件要执行的某个方法

    4.为什么点击按钮后就直接将占位图移除?

    #pragma mark - 重新加载按钮点击
    /** 重新加载按钮点击 */
    - (void)reloadButtonClicked:(UIButton *)sender{
        // 代理方执行方法
        if ([_delegate respondsToSelector:@selector(placeholderView:reloadButtonDidClick:)]) {
            [_delegate placeholderView:self reloadButtonDidClick:sender];
        }
        // 从父视图上移除
        [self removeFromSuperview];
    }
    

    我之前的做法是写一个移除占位图的方法,然后将这个方法暴露出去,需要移除的时候就调用这个方法。后面想了想,这样做完全是自找麻烦:我们点击UIAlertView的按钮的时候,这个alertView不就自动移除了吗?而这个占位图,不也可以看做是一个弹窗吗?

    为何这么6

    使用方法

    作为标准delegate传值的view,使用方法类似于系统的UIAlertView
    1. 引入delegate

    @interface ViewController ()<CQPlaceholderViewDelegate>
    

    2. 初始化

    CQPlaceholderView *placeholderView = [[CQPlaceholderView alloc]initWithFrame:tableView.bounds type:CQPlaceholderViewTypeNoOrder delegate:self];
    [tableView addSubview:placeholderView];
    

    看到没有,我想在哪add就在哪add,比那什么只能在tableView或collectionView上展示的强大多了。正是这个frame和完全开放的被add性决定了这个通用占位图的高度灵活性。


    推了一下我的300多度近视眼镜

    3. 处理回调

    #pragma mark - Delegate - 占位图
    /** 占位图的重新加载按钮点击时回调 */
    - (void)placeholderView:(CQPlaceholderView *)placeholderView reloadButtonDidClick:(UIButton *)sender{
        switch (placeholderView.type) {
            case CQPlaceholderViewTypeNoGoods:       // 没商品
            {
                [self.view makeToast:@"买个球啊"];
            }
                break;
                
            case CQPlaceholderViewTypeNoOrder:       // 没有订单
            {
                [self.view makeToast:@"拉到就拉到"];
            }
                break;
                
            case CQPlaceholderViewTypeNoNetwork:     // 没网
            {
                [self.view makeToast:@"没网适合打排位"];
            }
                break;
                
            case CQPlaceholderViewTypeBeautifulGirl: // 妹纸
            {
                [self.view makeToast:@"哦,那你很棒棒哦"];
            }
                break;
                
            default:
                break;
        }
    }
    

    对比DZNEmptyDataSet

    DZNEmptyDataSet
    这是当前很受欢迎的一个库:

    A drop-in UITableView/UICollectionView superclass category for showing empty datasets whenever the view has no content to display

    它通过一系列代理方法来决定占位图的显示样式及事件回调。平心而论,很优秀,但是我还是决定用我自己的。
    我的看法是(Im my opinion):
    DZNEmptyDataSet很强大,它之所以如此强大是为了让大部分人可以直接拿来即可用,它的目标是满足大部分开发者的一般常规需求,所以,它或许适合你,但一定不是最适合你,最适合你的,一定是自己亲手打造的。还有,它很强大,以至于有些功能你都不需要。
    窃以为:知道其实现原理,然后自己封装真正适合自己当前项目的框架才是王道

    多了些细节

    demo

    分享的人不少,认真总结分享的人不多,请不要吝惜你的star。
    github demo

    只有107个了

    相关文章

      网友评论

      • 叶小合:你好我在pod 'ReactiveCocoa','~>2.5.0',之后编译项目找不到找不到 “metamacros.h” 文件
      • Arthurcsh:楼主写的真详细,看得出非常用心,图文并茂、栩栩如生,但我认为UIView add/remove对于复杂频繁的刷新可能会性能上影响,我的方案是抽象公共层固定置于UITableView,采用隐藏/显示动态切换占位图几相关信息。如有更好的方法敬请共享,谢谢
      • 洛少城:[tableView addSubview:placeholderView]; 你直接把placeholderView添加到表格上, 如果在表格有数据时, 数据源控制得不准确时,你推动表格, 你的placeholderView会直接被拖到屏幕外面去了
        Lol刀妹:可以试试[tableView.superView addSubView:palceHolderView] :smile:
      • 杏仁丶::yum: 就喜欢你这样分享的~
        Lol刀妹:@杏仁丶 哈哈,共同进步:smile:
      • 雪_晟:楼主,怎么监听网络状态通知所有的页面添加无网络占位图呢
        Lol刀妹:@雪_晟 在app delegate 里面就把网络监控开启
        雪_晟:@无夜之星辰 感觉不太准 我用的AFN
        Lol刀妹:@雪_晟 在基类里面监听
      • Link913:配图很6
        Lol刀妹:@SkyHarute 哈哈:smile:
      • smkoc:跟我写的自己写的差不多 我尼玛觉得配图挺牛的 107
        Lol刀妹:@smkoc 配图是精髓
      • 伦敦乡下的小作家:我主要是来看配图的:smirk:
        Lol刀妹:@伦敦乡下的小作家 配图是精华:sunglasses:
      • moonCoder:66666
        Lol刀妹:@moonCoder 欧阳大神带我飞啊

      本文标题:iOS开发造轮子 | 通用占位图

      本文链接:https://www.haomeiwen.com/subject/bniqxxtx.html