iOS搜索框下的标签选择Tag,或者行业选择,标签选择(UIButton实现),iOS布局

最近一直忙两个项目,有段时间没有学习新东西了,感觉已经out了.项目中用到一个行业选择的功能.(本文参照SKTagView编写,不知道出自哪位大神)。效果图:↓
(代码纯属copy,如有不适,同志,请坚持看完!)


TaoBaoSearch.jpeg

标签选择.png

我开始的思路是用collectionView去实现,不过总觉的不太好,我这里需求是单选,总觉得交互会很麻烦,所以就扒了点代码,看了大神们的实现思路模仿一下。

整体思路:首先是有三个组成部分分别是;

Model : 来存储所创建的每个标签的属性(后续TagModel就代表Model);
View : 盛放标签的View,类中计算标签的行数高度,初始化方法,便利构造器等一系列(后续TagView就代表这个View);
Button: 自定义Btn通过Model进行相应设置,在View中创建Button;

主题流程就是:

1.创建TagView
2.遍历数据源(需要展示的所有标签)创建TagModel(注意此时TagView只是创建视图并没有创建视图上的标签或者说Btn),当创建一个TagModel我们就去通知TagView去根据这个TagModel(包含btn属性)去创建Btn。下图解:↓

注解.png

下面代码:

TagModel
#DzTag.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface DzTag : NSObject
@property (copy, nonatomic, nullable) NSString *text;
// 需要设置Btn的title属性时候用这个
@property (copy, nonatomic, nullable) NSAttributedString *attributedText;
// 字体颜色
@property (strong, nonatomic, nullable) UIColor *textColor;
// btn背景色
@property (strong, nonatomic, nullable) UIColor *bgColor;
// 高亮背景色
@property (strong, nonatomic, nullable) UIColor *highlightedBgColor;
// 背景图片
@property (strong, nonatomic, nullable) UIImage *bgImg;
@property (strong, nonatomic, nullable) UIImage *sebgColor;
// 圆角
@property (assign, nonatomic) CGFloat cornerRadius;
// 边框颜色
@property (strong, nonatomic, nullable) UIColor *borderColor;
// 边框宽度
@property (assign, nonatomic) CGFloat borderWidth;
// 内边距
@property (assign, nonatomic) UIEdgeInsets padding;
@property (strong, nonatomic, nullable) UIFont *font;
@property (assign, nonatomic) CGFloat fontSize;
//默认:YES
@property (assign, nonatomic) BOOL enable;
- (nonnull instancetype)initWithText: (nonnull NSString *)text;
+ (nonnull instancetype)tagWithText: (nonnull NSString *)text;
@end

#DzTag.m
#import "DzTag.h"
static const CGFloat kDefaultFontSize = 13.0;
@implementation DzTag
- (instancetype)init {
    self = [super init];
    if (self) {
        _fontSize   = kDefaultFontSize;
        _textColor  = [UIColor blackColor];
        _bgColor    = [UIColor whiteColor];
        _enable     = YES;
    }
    return self;
}
// 初始化方法
- (instancetype)initWithText: (NSString *)text {
    self = [self init];
    if (self) {
        _text = text;
    }
    return self;
}
// 遍历构造器
+ (instancetype)tagWithText: (NSString *)text {
    return [[self alloc] initWithText: text];
}
@end
自定义Button
#DzTagButton.h

#import <UIKit/UIKit.h>
@class DzTag;
@interface DzTagButton : UIButton
+ (nonnull instancetype)buttonWithTag: (nonnull DzTag *)tag;
@end

#DzTagButton.m
#import "DzTagButton.h"
#import "DzTag.h"

@implementation DzTagButton

// 创建Button,并且设置Button属性
+ (instancetype)buttonWithTag: (DzTag *)tag {
    // 创建
    DzTagButton *btn = [super buttonWithType:UIButtonTypeCustom];
    // 是否使用attributedText
    if (tag.attributedText) {
        [btn setAttributedTitle: tag.attributedText forState: UIControlStateNormal];
    } else {
        [btn setTitle: tag.text forState:UIControlStateNormal];
        [btn setTitleColor: tag.textColor forState: UIControlStateNormal];
        btn.titleLabel.font = tag.font ?: [UIFont systemFontOfSize: tag.fontSize];
    }
    btn.backgroundColor = tag.bgColor;
    btn.contentEdgeInsets = tag.padding;
    btn.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
    // 设置背景图
    if (tag.bgImg) {
        [btn setBackgroundImage: tag.bgImg forState: UIControlStateNormal];
    }
    // 设置颜色
    if (tag.sebgColor) {
        [btn setBackgroundImage:tag.sebgColor forState:(UIControlStateSelected)];
    }
    // 设置边框颜色
    if (tag.borderColor) {
        btn.layer.borderColor = tag.borderColor.CGColor;
    }
    // 设置变宽宽度
    if (tag.borderWidth) {
        btn.layer.borderWidth = tag.borderWidth;
    }
    // 是否启用
    btn.userInteractionEnabled = tag.enable;
    // 是否要高亮效果
    if (tag.enable) {
        UIColor *highlightedBgColor = tag.highlightedBgColor ?: [self darkerColor:btn.backgroundColor];
        [btn setBackgroundImage:[self imageWithColor:highlightedBgColor] forState:UIControlStateHighlighted];
    }
    // Btn圆角
    btn.layer.cornerRadius = tag.cornerRadius;
    btn.layer.masksToBounds = YES;
    return btn;
}
// 根据颜色生成图片
+ (UIImage *)imageWithColor:(UIColor *)color {
    CGRect rect = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f);
    UIGraphicsBeginImageContext(rect.size);
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    CGContextSetFillColorWithColor(context, [color CGColor]);
    CGContextFillRect(context, rect);
    
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    return image;
}
// 如果有颜色就返回,没有就不要
+ (UIColor *)darkerColor:(UIColor *)color {
    CGFloat h, s, b, a;
    if ([color getHue:&h saturation:&s brightness:&b alpha:&a])
        return [UIColor colorWithHue:h
                          saturation:s
                          brightness:b * 0.85
                               alpha:a];
    return color;
}
@end
TagView
#DzTagView.h

#import <UIKit/UIKit.h>
#import "DzTag.h"
@class DzTagButton;
@interface DzTagView : UIView
#pragma mark -- 属性
// 视图内边距
@property (assign, nonatomic) UIEdgeInsets padding;
// 行间距
@property (assign, nonatomic) CGFloat lineSpacing;
// 每个item间距
@property (assign, nonatomic) CGFloat interitemSpacing;
// 最大宽
@property (assign, nonatomic) CGFloat preferredMaxLayoutWidth;
//!< 固定宽度
@property (assign, nonatomic) CGFloat regularWidth;
//!< 固定高度
@property (nonatomic,assign ) CGFloat regularHeight;
// 单行模式
@property (assign, nonatomic) BOOL singleLine;
// block点击回调
@property (copy, nonatomic, nullable) void (^didTapTagAtIndex)(NSUInteger index,UIButton * _Nullable btn);

#pragma mark -- 方法
// 创建方法
- (void)addTag: (nonnull DzTag *)tag;
// 添加item(指定位置添加)
- (void)insertTag: (nonnull DzTag *)tag atIndex:(NSUInteger)index;
// 移除item
- (void)removeTag: (nonnull DzTag *)tag;
// 根据位置移除
- (void)removeTagAtIndex: (NSUInteger)index;
// 移除所有
- (void)removeAllTags;
@end

#DzTagView.m代码比较多我先总结下结构和声明周期.
.m一共分三大部分 :↓
①:生命周期。
②:getter,setter。
③:一些共有方法包括创建·添加·删除item等。
其他解释我想类中注释够用了,几乎每行都有了😆
#核心:重写 intrinsicContentSize 为内容返回恰当的大小,无论何时有任何会影响固有内容尺寸的改变发生时,调用 invalidateIntrinsicContentSize进行更新(如果想要在app运行时改变 intrinsicContentSize,可以调用invalidateIntrinsicContentSize()方法来更新)
#DzTagView.m

#import "DzTagView.h"
#import "DzTagButton.h"
@interface DzTagView ()
@property (strong, nonatomic, nullable) NSMutableArray *tags;
//用来表示是否需要重新加载一次,一般添加或者删除item时候改变此值状态,用来重新加载,节省内存使用
@property (assign, nonatomic) BOOL didSetup;
@property (nonatomic,assign) BOOL isIntrinsicWidth;  //!<是否宽度固定
@property (nonatomic,assign) BOOL isIntrinsicHeight; //!<是否高度固定
@end
@implementation DzTagView

#pragma mark - public方法
// NSParameterAssert() ↓
// 断言评估一个条件,如果条件为 false
// 调用当前线程的断点句柄
// 每一个线程有它自已的断点句柄
// 它是一个 NSAsserttionHandler类的对象。
// 当被调用时,断言句柄打印一个错误信息,该条信息中包含了方法名、类名或函数名。
// 然后,它就抛出一个 NSInternalInconsistencyException 异常

// 创建item方法
- (void)addTag: (DzTag *)tag {
    // 断言
    NSParameterAssert(tag);
    DzTagButton *btn = [DzTagButton buttonWithTag: tag];
    // 添加事件
    [btn addTarget: self action: @selector(onTag:) forControlEvents: UIControlEventTouchUpInside];
    [self addSubview: btn];
    [self.tags addObject: tag];
    // 更新布局
    self.didSetup = NO;
    [self invalidateIntrinsicContentSize];
}

// 在某位置添加tag
- (void)insertTag: (DzTag *)tag atIndex: (NSUInteger)index {
    // 断言
    NSParameterAssert(tag);
    // 如果这个位置是在最后位置直接添加
    if (index + 1 > self.tags.count) {
        [self addTag: tag];
    } else {
        // 创建BTN
        DzTagButton *btn = [DzTagButton buttonWithTag: tag];
        // 添加事件
        [btn addTarget: self action: @selector(onTag:) forControlEvents: UIControlEventTouchUpInside];
        // 相应位置添加子视图
        [self insertSubview: btn atIndex: index];
        // 数据源相应位置添加tag
        [self.tags insertObject: tag atIndex: index];
        // 更新布局
        self.didSetup = NO;
        [self invalidateIntrinsicContentSize];
    }
}
// 根据tag删除item
- (void)removeTag: (DzTag *)tag {
    // 断言
    NSParameterAssert(tag);
    // 根据tag获取相应index
    NSUInteger index = [self.tags indexOfObject: tag];
    //NSNotFound表示请求操作的某个内容或者item没有发现,或者不存在。
    if (NSNotFound == index) {
        return;
    }
    // 删除数据和UI
    [self.tags removeObjectAtIndex: index];
    if (self.subviews.count > index) {
        [self.subviews[index] removeFromSuperview];
    }
    // 重新布局
    self.didSetup = NO;
    [self invalidateIntrinsicContentSize];
}

// 根据index删除某个item
- (void)removeTagAtIndex: (NSUInteger)index {
    // 越界保护
    if (index + 1 > self.tags.count) {
        return;
    }
    // 删除相应item
    [self.tags removeObjectAtIndex: index];
    // 删除UI
    if (self.subviews.count > index) {
        [self.subviews[index] removeFromSuperview];
    }
    // 重新布局
    self.didSetup = NO;
    [self invalidateIntrinsicContentSize];
}
// 删除所有item
- (void)removeAllTags {
    // 先删除数据源
    [self.tags removeAllObjects];
    // 删除所有UI子视图
    for (UIView *view in self.subviews) {
        [view removeFromSuperview];
    }
    // 重新布局
    self.didSetup = NO;
    [self invalidateIntrinsicContentSize];
}

#pragma mark - 点击item响应事件
- (void)onTag: (UIButton *)btn {
    if (self.didTapTagAtIndex) {
        self.didTapTagAtIndex([self.subviews indexOfObject: btn], btn);
    }
}
#pragma mark -- 生命周期
//TODO: 生命周期开始 第①步
// UIView的属性intrinsicContentSize 返回一个CGSize(用来在外部计算高度)
-(CGSize)intrinsicContentSize {
    // 如果没有数据源一个item都没有则返回长宽高位置都是0
    if (!self.tags.count) {
        return CGSizeZero;
    }
    // 计算大小需要的属性
    NSArray *subviews = self.subviews;
    // 前一视图
    UIView  *previousView = nil;
    // 上边距
    CGFloat topPadding = self.padding.top;
    // 下边距
    CGFloat bottomPadding = self.padding.bottom;
    // 左边距
    CGFloat leftPadding = self.padding.left;
    // 右边距
    CGFloat rightPadding = self.padding.right;
    // item间距
    CGFloat itemSpacing = self.interitemSpacing;
    // 行间距
    CGFloat lineSpacing = self.lineSpacing;
    // 当前X
    CGFloat currentX = leftPadding;
    // 视图内在视图高度 / 最后返回
    CGFloat intrinsicHeight = topPadding;
    // 视图内在视图宽   / 最后返回
    CGFloat intrinsicWidth = leftPadding;
    
    // 如果非单行显示 并且最大宽度大于0
    if (!self.singleLine && self.preferredMaxLayoutWidth > 0) {
        
        // 行数
        NSInteger lineCount = 0;
        // 遍历subViews
        for (UIView *view in subviews) {
            // 获取子view的size
            CGSize size = view.intrinsicContentSize;
            // 宽度和高度通过参数的0或者非0来进行赋值,却别是否用固定宽度(三目)
            CGFloat width = self.isIntrinsicWidth ? self.regularWidth : size.width;
            CGFloat height = self.isIntrinsicHeight ? self.regularHeight: size.height;
            // 如果已经有item存在
            if (previousView) {
                // 确定x
                currentX += itemSpacing;
                // 如果当前itemX + 新item宽度 + 右边距 < 视图最大宽度
                if (currentX + width + rightPadding <= self.preferredMaxLayoutWidth) {
                    // 本行排列
                    currentX += width;
                } else { // // 如果当前itemX + 新item宽度 + 右边距 < 视图最大宽度 则 换行添加
                    // 行数自加1
                    lineCount ++;
                    // 跟新x
                    currentX = leftPadding + width;
                    // 更新View高度
                    intrinsicHeight += height;
                }
            } else { // 添加第一个item会走
                lineCount ++;
                // 更新宽高
                intrinsicHeight += height;
                currentX += width;
            }
            // 赋值前一view
            previousView = view;
            // 更新宽度
            intrinsicWidth = MAX(intrinsicWidth, currentX + rightPadding);
        }
        // 计算最终高度
        intrinsicHeight += bottomPadding + lineSpacing * (lineCount - 1);
    } else { // 单行显示时候计算size
        for (UIView *view in subviews) {
            CGSize size = view.intrinsicContentSize;
            intrinsicWidth += self.isIntrinsicWidth ? self.regularWidth : size.width;
        }
        // 最终宽高
        intrinsicWidth += itemSpacing * (subviews.count - 1) + rightPadding;
        intrinsicHeight += ((UIView *)subviews.firstObject).intrinsicContentSize.height + bottomPadding;
    }
    // 返回一个CGSize
    return CGSizeMake(intrinsicWidth, intrinsicHeight);
}

//TODO: 生命周期 第②步
- (void)layoutSubviews {
    // 非单行显示
    if (!self.singleLine) {
        self.preferredMaxLayoutWidth = self.frame.size.width;
    }
    [super layoutSubviews];
    [self layoutTags];
}

//TODO: 生命周期 第③步
// 内部布局(实现过程等同于intrinsicContentSize方法中的计算)
- (void)layoutTags {
    if (self.didSetup || !self.tags.count) {
        return;
    }
    NSArray *subviews    = self.subviews;
    UIView *previousView = nil;
    CGFloat topPadding   = self.padding.top;
    CGFloat leftPadding  = self.padding.left;
    CGFloat rightPadding = self.padding.right;
    CGFloat itemSpacing  = self.interitemSpacing;
    CGFloat lineSpacing  = self.lineSpacing;
    CGFloat currentX = leftPadding;
    if (!self.singleLine && self.preferredMaxLayoutWidth > 0) {
        for (UIView *view in subviews) {
            CGSize size = view.intrinsicContentSize;
            CGFloat width1 = self.isIntrinsicWidth?self.regularWidth:size.width;
            CGFloat height1 = self.isIntrinsicHeight?self.regularHeight:size.height;
            if (previousView) {
                //                CGFloat width = size.width;
                currentX += itemSpacing;
                if (currentX + width1 + rightPadding <= self.preferredMaxLayoutWidth) {
                    view.frame = CGRectMake(currentX, CGRectGetMinY(previousView.frame), width1, height1);
                    currentX += width1;
                } else {
                    CGFloat width = MIN(width1, self.preferredMaxLayoutWidth - leftPadding - rightPadding);
                    view.frame = CGRectMake(leftPadding, CGRectGetMaxY(previousView.frame) + lineSpacing, width, height1);
                    currentX = leftPadding + width;
                }
            } else {
                CGFloat width = MIN(width1, self.preferredMaxLayoutWidth - leftPadding - rightPadding);
                view.frame = CGRectMake(leftPadding, topPadding, width, height1);
                currentX += width;
            }
            previousView = view;
        }
    } else {
        for (UIView *view in subviews) {
            CGSize size = view.intrinsicContentSize;
            view.frame = CGRectMake(currentX, topPadding, self.isIntrinsicWidth?self.regularWidth:size.width, self.isIntrinsicHeight?self.regularHeight:size.height);
            currentX += self.isIntrinsicWidth?self.regularWidth:size.width;
            previousView = view;
        }
    }
    self.didSetup = YES;
}

#pragma mark - setting getter方法
// 数据源
- (NSMutableArray *)tags {
    if(!_tags) {
        _tags = [NSMutableArray array];
    }
    return _tags;
}
// 最大宽度setter方法
- (void)setPreferredMaxLayoutWidth: (CGFloat)preferredMaxLayoutWidth {
    if (preferredMaxLayoutWidth != _preferredMaxLayoutWidth) {
        _preferredMaxLayoutWidth = preferredMaxLayoutWidth;
        _didSetup = NO;
        [self invalidateIntrinsicContentSize];
    }
}
// 重写setter给bool赋值
- (void)setRegularWidth:(CGFloat)intrinsicWidth{
    if (_regularWidth != intrinsicWidth) {
        _regularWidth = intrinsicWidth;
        if (intrinsicWidth == 0) {
            self.isIntrinsicWidth = NO;
        }else{
            self.isIntrinsicWidth = YES;
        }
    }
}
- (void)setRegularHeight:(CGFloat)intrinsicHeight{
    if (_regularHeight != intrinsicHeight) {
        _regularHeight = intrinsicHeight;
        if (intrinsicHeight == 0){
            self.isIntrinsicHeight = NO;
        }
        else{
            self.isIntrinsicHeight = YES;
        }
    }
}

@end

Demo下载地址:https://github.com/rundonkey/DzTagView.git

Eed

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

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,401评论 25 707
  • 什么都是听说 什么都在听说 你听说了我的过往 我听说了你 你不要听说 我也不要你
    十仁阅读 170评论 1 0
  • 其实早在好几年前就在书店看到《秘密》这本书,当时只是单纯觉得这本书的字好少,就没有买。没想到,这一错过就是几年的时...
    幸福储钱罐阅读 187评论 0 1
  • Api介绍: Defines whether the ViewGroup will clip its childr...
    DevWang阅读 14,761评论 1 57