iOS 模仿某宝商品规格选择弹框

前言:

代码生涯里总能碰到一些稍微复杂点的东西,先是花时间弄出来了,然后又发现跟不上需求了,调整了一下代码思路,然后是重构重写,最后应该是丢弃了这份代码。在这里做个笔记,下辈子应该也用不上。

1.我们这是一个卖电子产品的项目
2.我们有一个选择商品规格的页面(选颜色,内存,套餐,数量...)
3.我们打算抄袭某宝的弹框
4.这是目前我觉得最好的方案去实现这个页面
5.过了两个星期,需求又改了,有些商品规格文字超长,要做多行显示。(***)

效果图


商品选择.gif

其实我们的业务逻辑有点复杂,本次代码只是研究一下,这样的 UI 如何去写。大体上就跟 UITableView 一样。主要有两个类, 一个类 继承自 UIScrollView, 一个类继承自 UIView。

以下是代码的重点部分,次要的代码没有贴出来,完整的示例代码在 https://github.com/gityuency/ObjectiveCTools

上代码: ItemView.h

1.协议:ItemViewDelegate 是当你点击了这个小格子触发的代理方法
2.暴露 NSIndexPath 这个属性,这个方便索引当前view 位置(第几行的第几个)
3.暴露选中和未选中属性,这个可以用来设置选中和未选中的样式
4.暴露不可点击属性,用于设置不可点击样式
5.暴露方法用于设置文本,字体,宽度,这是影响这个 View 大小的因素,所以放到外面设置。

#import <UIKit/UIKit.h>
@class ItemView;

/// 视图的协议
@protocol ItemViewDelegate <NSObject>

@optional
-(void)didSelected:(ItemView *)itemView;

@end

/// 格子
@interface ItemView : UIView
/// 按钮事件代理
@property (nonatomic, weak) id<ItemViewDelegate> delegate;
/// 是否是多行
@property (nonatomic, assign) BOOL isMultiLines;
/// 位置
@property (nonatomic, strong) NSIndexPath *indexPath;
/// 设置选中状态
@property (nonatomic, assign) BOOL itemSelected;
/// 设置不可选
@property (nonatomic, assign) BOOL itemDisable;
/// 设置 文本框的文字 文字的最小宽度 最大宽度 文字的字体
- (void)setText:(NSString *)text minWith:(CGFloat)minWith maxWith:(CGFloat)maxWith font:(UIFont *)font;

@end
ItemView.m

1.添加一个点击手势,子视图的用户交互都关闭
2.在设置视图文字的时候,计算他们的宽度
3.重写 setter 方法,去改变样式

#import "ItemView.h"

@interface ItemView ()

/// 文本框
@property (nonatomic, strong) UILabel *label;
/// 装载内容
@property (nonatomic, strong) UIView *contentView;
/// 最小宽度
@property (nonatomic, assign) CGFloat *minWidth;
/// 选中的标题颜色  边框色
@property (nonatomic, strong) UIColor *selectedTitleColro;
/// 选中背景色
@property (nonatomic, strong) UIColor *selectedBgColor;
/// 未选中背景色
@property (nonatomic, strong) UIColor *unSelectedBgColor;
/// 未选中的标题颜色
@property (nonatomic, strong) UIColor *unSelectedTitleColor;
/// 不可点击的标题颜色
@property (nonatomic, strong) UIColor *disableTitleColor;
/// 点击事件(手势)
@property (nonatomic, strong) UITapGestureRecognizer *g;

@end

@implementation ItemView

- (instancetype)init {
    
    self = [super init];
    if (self) {
        
        _disableTitleColor = [UIColor grayColor];
        _selectedBgColor = [UIColor colorWithRed:1.00 green:0.91 blue:0.91 alpha:1.00];
        _unSelectedBgColor = [UIColor colorWithRed:0.95 green:0.95 blue:0.95 alpha:1.00];
        _unSelectedTitleColor = [UIColor darkTextColor];
        _selectedTitleColro = [UIColor redColor];
        _minWidth = 0;
        
        _contentView = [[UIView alloc] init];
        _contentView.userInteractionEnabled = NO;
        _contentView.layer.cornerRadius = 5;
        [self addSubview:_contentView];
        
        _label = [[UILabel alloc] init];
        _label.textAlignment = NSTextAlignmentCenter;
        _label.userInteractionEnabled = NO;
        _label.numberOfLines = 0;
        [self addSubview:_label];
        
        _isMultiLines = NO;

        _g = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(click)];
        _g.numberOfTapsRequired = 1;
        [self addGestureRecognizer:_g];
    }
    return self;
}

- (void)click{
    if (self.delegate && [self.delegate respondsToSelector:@selector(didSelected:)]) {
        [self.delegate didSelected:self];
    }
}

/// 设置 文本框的文字 文字的最小宽度 文字的字体
- (void)setText:(NSString *)text minWith:(CGFloat)minWith maxWith:(CGFloat)maxWith font:(UIFont *)font {
    CGFloat offset = 10;
    
    NSDictionary *dic = @{NSFontAttributeName: font};
    
    CGFloat maxW = [UIScreen mainScreen].bounds.size.width - offset * 2;
    
    CGSize stringSize = [text boundingRectWithSize:CGSizeMake(maxW, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:dic context:nil].size;
    
    CGFloat singleLineWidth = [text sizeWithAttributes:dic].width;  //计算单行字符串长度
    
    if (singleLineWidth > maxW) { //判断是否多行
        stringSize.width = maxW;
        self.isMultiLines = YES;
    } else if (stringSize.width < minWith) {  //判断是否小于最小宽度
        stringSize.width = minWith;
    }

    self.label.frame = CGRectMake(offset, offset, stringSize.width, stringSize.height);
    self.contentView.frame = CGRectMake(offset * 0.5, offset * 0.5, stringSize.width + offset, stringSize.height + offset);
    self.frame = CGRectMake(0, 0, self.label.frame.size.width + offset * 2, self.label.frame.size.height + offset * 2);
    self.label.text = text;
    self.label.font = font;
}

- (void)setItemSelected:(BOOL)itemSelected {
    _itemSelected = itemSelected;
    if (_itemSelected) {
        [self.g setEnabled:YES];
        self.contentView.layer.borderWidth = 1;
        self.contentView.layer.borderColor = self.selectedTitleColro.CGColor;
        self.contentView.backgroundColor = self.selectedBgColor;
        self.label.textColor = self.selectedTitleColro;
    } else {
        [self.g setEnabled:YES];
        self.contentView.layer.borderWidth = 0;
        self.contentView.backgroundColor = self.unSelectedBgColor;
        self.label.textColor = self.unSelectedTitleColor;
    }
}

- (void)setItemDisable:(BOOL)itemDisable {
    _itemDisable = itemDisable;
    if (_itemDisable) {
        [self.g setEnabled:NO];
        self.contentView.layer.borderWidth = 0;
        self.contentView.backgroundColor = self.unSelectedBgColor;
        self.label.textColor = self.disableTitleColor;
    }
}

@end
ItemCollectionView.h

1.仿照 UITableView 写一下代理,必选方法得到 个数,格子是什么, 可选方法得到 头视图,点击事件,行数。
2.暴露一个方法用于创建这个视图。

#import <UIKit/UIKit.h>
#import "ItemView.h"

@class ItemCollectionView;

/// 视图的协议
@protocol ItemViewDataSource <NSObject>
@required
/// 每一个格子是什么
- (ItemView *)itemCollectionView:(ItemCollectionView *)itemCollectionView cellForRowAtIndexPath:(NSIndexPath *)indexpath;
/// 每一行有多少个格子
- (NSInteger)itemCollectionView:(ItemCollectionView *)itemCollectionView numberOfRowsInSection:(NSInteger)section;
@optional
/// 一共有多少行
- (NSInteger)numberOfSectionsAt:(ItemCollectionView *)itemCollectionView;
/// 设置每一行的头视图
- (UIView *)itemCollectionView:(ItemCollectionView *)itemCollectionView headerInSection:(NSInteger)section;
/// 点击事件
- (void)itemCollectionView:(ItemCollectionView *)itemCollectionView didSelectedIndexPath:(NSIndexPath *)indexpath;
@end

/// 装载所有的小格子
@interface ItemCollectionView : UIScrollView
/// 数据代理
@property (nonatomic, weak) id<ItemViewDataSource> dataSource;
/// 重新创作视图
- (void)createCollectionView;
@end
ItemCollectionView.m
  1. 考虑到初始化可能使用代码或者 XIB,所以两个初始化经过的函数都调用了一个公共的初始化属性方法 setUp

2.在方法 createCollectionView 里面先强制进行布局,来得到实际的尺寸,因为从 XIB 加载会有情况还没拿到尺寸就开始布局了

3.在initView方法里面创建这些小格子。考虑到转屏等一些影响布局的因素, 每次进这个函数的时候,就把原来已经创建的格子全部扔掉,重新再来。这当然是一个不好的设计,但是没有去优化了。如果因为数据的改变(比如文字长度变了)那还是得重新算,干脆都重新算,少写点代码。

4.两层For循环,然后从代理函数里面去取得这个格子,就可以拿到这个格子的尺寸。当然个数也都能拿到。把这些小格子都放到数组里面。

5.当有格子被点击了,点击的格子设置为选中状态,其余的格子设置为未选中状态。在小格子的代理方法里面, 直接取得小格子属性 indexPath.section, 然后在小格子数字里找到对应的行,循环遍历,先全都设置为未选中(不可选的格子跳过),再把点击的格子设置为选中。

#import "ItemCollectionView.h"

@interface ItemCollectionView () <ItemViewDelegate>

/// 内容视图
@property (nonatomic, strong) UIView *contentView;
/// 每一行有多少个 默认 0 个
@property (nonatomic, assign) CGFloat rowCountOfSection;
/// 一共有多少行 默认1行
@property (nonatomic, assign) CGFloat sectionCount;
/// 临时数组
@property (nonatomic, strong) NSMutableArray *viewsArray;
/// 内部控件集合
@property (nonatomic, strong) NSArray *itemViewArray;

@end

@implementation ItemCollectionView

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self setUp];
    }
    return self;
}

- (void)awakeFromNib {
    [super awakeFromNib];
    [self setUp];
}

- (void)setUp {
    _rowCountOfSection = 0;
    _sectionCount = 1;
    _viewsArray = [NSMutableArray array];
}

- (void)initView {
    
    if (self.contentView != nil) {
        [self.contentView removeFromSuperview];
    }
    self.contentView = [[UIView alloc] init];
    [self addSubview:self.contentView];
    
    /// 有多少行, 可选代理
    if (self.dataSource && [self.dataSource respondsToSelector:@selector(numberOfSectionsAt:)]) {
        self.sectionCount = [self.dataSource numberOfSectionsAt:self];
    }
    
    /// 每一行有多少个格子,必选
    if (self.dataSource) {
        BOOL a = [self.dataSource respondsToSelector:@selector(itemCollectionView:numberOfRowsInSection:)];
        BOOL b = [self.dataSource respondsToSelector:@selector(itemCollectionView:cellForRowAtIndexPath:)];
        NSAssert((a && b), @"代理对象没有遵守协议!");
    } else {
        NSAssert(NO, @"代理对象为空!");
    }
    
    CGFloat allSectionHeight = 0;
    for (NSInteger i = 0; i < self.sectionCount; i++) {
        UIView *sectionView = [[UIView alloc] init];
        UIView *headView = [self.dataSource itemCollectionView:self headerInSection:i];
        headView.frame = CGRectMake(headView.frame.origin.x, 0, self.bounds.size.width, headView.frame.size.height);
        [sectionView addSubview:headView];
        CGFloat offsetY = headView.frame.size.height;
        UIView *lastrowCell;
        
        self.rowCountOfSection = [self.dataSource itemCollectionView:self numberOfRowsInSection:i];
        
        NSMutableArray *array = [NSMutableArray array];
        
        ItemView *lastCell = nil; //最后一个单元
        
        for (NSInteger k = 0; k < self.rowCountOfSection; k++) {
            NSIndexPath *index = [NSIndexPath indexPathForRow:k inSection:i];
            ItemView *rowCell = [self.dataSource itemCollectionView:self cellForRowAtIndexPath: index];
            rowCell.delegate = self;
            rowCell.indexPath = index;
            
            
            if (lastCell) {  //最后一个单元存在
                
                if (CGRectGetMaxX(lastCell.frame) + rowCell.frame.size.width > self.bounds.size.width) { //新来的这个在后面放不下
                    
                    offsetY += lastCell.frame.size.height;  //放不下就放到下一行
                    
                    rowCell.frame = CGRectMake(0, offsetY, rowCell.frame.size.width, rowCell.frame.size.height);
                    
                } else {
                    
                    if (lastCell.isMultiLines) { //上一个是多行, 当前就另起一行
                        offsetY += lastCell.frame.size.height;
                    }
                    
                    rowCell.frame = CGRectMake(CGRectGetMaxX(lastCell.frame), offsetY, rowCell.frame.size.width, rowCell.frame.size.height);
                }
                
            } else { //最后一个单元不存在
                
                rowCell.frame = CGRectMake(0, offsetY, rowCell.frame.size.width, rowCell.frame.size.height);
            }
            
            lastCell = rowCell;

            
            [sectionView addSubview:rowCell];
            lastrowCell = rowCell;
            [array addObject:rowCell];
        }
        CGFloat sectionHeight = CGRectGetMaxY(lastrowCell.frame) + 5; // +5是为了底部多留点空白
        sectionView.frame = CGRectMake(0, allSectionHeight, self.bounds.size.width, sectionHeight);
        [self.contentView addSubview:sectionView];
        allSectionHeight += sectionHeight;
        
        if (i != self.sectionCount - 1) { //最后一行不要分割线
            UIView *line = [[UIView alloc] initWithFrame:CGRectMake(0, sectionView.frame.size.height - 1, sectionView.frame.size.width, 1)];
            line.backgroundColor = [UIColor darkTextColor];
            [sectionView addSubview:line];
        }
        [self.viewsArray addObject:array];
    }
    self.contentView.frame = CGRectMake(0, 0, self.bounds.size.width, allSectionHeight);
    self.contentSize = self.contentView.frame.size;
    self.itemViewArray = self.viewsArray;
}


#pragma mark - ItemViewDelegate
- (void)didSelected:(ItemView *)itemView {
    
    for (ItemView *v in self.itemViewArray[itemView.indexPath.section]) {
        if (v.itemDisable) { continue; }
        v.itemSelected = NO;
    }
    itemView.itemSelected = YES;
    
    if (self.dataSource && [self.dataSource respondsToSelector:@selector(itemCollectionView:didSelectedIndexPath:)]) {
        [self.dataSource itemCollectionView:self didSelectedIndexPath:itemView.indexPath];
    }
}

- (void)createCollectionView {
    [self setNeedsLayout];
    [self layoutIfNeeded];
    [self initView];
}

@end
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,476评论 2 59
  • 天气正好 微风不燥 脱下职装他开始长跑 街角的火锅店廉价美味 一双男女饮酒作对 怎么忽然笑容变眼泪 热气腾空又支离...
    咖喱鸡与阿花阅读 157评论 1 2
  • 我数着一点点,一天天,一念念的星: 星星告诉我,天上会有道光明; 我翻过山,穿过岭; 夜夜寻找那光明; 云深却不知...
    墨上梢头阅读 179评论 0 0
  • 家里下雪了 好想看雪花飘落 落在树上、行人上 最终落在地面上 好想出去淋场雪 让雪花肆意的落在自己身上 抬起头 融...
    秋天冬天阅读 105评论 0 0