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

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


iOS搜索框下的标签选择Tag,或者行业选择,标签选择(UIButton实现),iOS布局_第1张图片
TaoBaoSearch.jpeg

iOS搜索框下的标签选择Tag,或者行业选择,标签选择(UIButton实现),iOS布局_第2张图片
标签选择.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。下图解:↓

iOS搜索框下的标签选择Tag,或者行业选择,标签选择(UIButton实现),iOS布局_第3张图片
注解.png

下面代码:

TagModel
#DzTag.h
#import 
#import 
@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 
@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 
#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,每天进步一点点

你可能感兴趣的:(iOS搜索框下的标签选择Tag,或者行业选择,标签选择(UIButton实现),iOS布局)