iOS--简易型标签的实现(UICollectionView)

接到一个需求就是要实现标签组的显示和选择,如下图所示:


image

一开始感觉没有什么头绪,参考网上各种demo,发现大部分的demo都是以自绘制标签为主实现标签的长度计算和自动换行,但是这样需要实现的计算量就非常大,对于一部分参考和后期维护起来就非常麻烦,稍微修改错一个参数,导致计算不准确,这就不太好实现。

但是想了一下我们常用的系统控件中,是否有相关的控件可以实现呢?第一个想法就让我想到了UICollectionView,既然UICollectionView能实现瀑布流,为什么标签这种无规则的界面不能实现呢?,于是就开始初步搭建:

首先,先了解在UICollectionView中如何能或者每一个cell的大小,想到我们常用的UITableView的操作,其实两者的用法基本也是一样的,所以在UICollectionViewDelegate中就有

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    NSString * containString = @"你想知道怎么实现宽度计算吗?";
    CGFloat width = [NSString getWidthWithText:containString height:30 font:13];
    return CGSizeMake(width + 25, 30.0f);
}

这个方法能手动设置cell的大小。
既然能手动设置cell的大小了,那这样实现起来就容易了,按照正常的流程设置UICollectionView的数据源方法和相关的代理方法即可。

接下来的就是一个封装好的类,即计算String的宽度:

/**
 根据高度度求宽度

 @param text 计算的内容
 @param height 计算的高度
 @param font 字体大小
 @return 返回宽度
 */
+ (CGFloat)getWidthWithText:(NSString *)text height:(CGFloat)height font:(CGFloat)font
{
    //加上判断,防止传nil等不符合的值,导致程序奔溃
    if (text == nil || [text isEqualToString:@""]){
        text = @"无";
    }
    if (font <= 0){
        font = 13;
    }
    if (height < 0){
        height = 0;
    }
    CGRect rect = [text boundingRectWithSize:CGSizeMake(MAXFLOAT, height)
                                     options:NSStringDrawingUsesLineFragmentOrigin
                                  attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:font]}
                                     context:nil];
    return rect.size.width;
}

这样大概的设置,大概即可实现在UICollectionView中的每一个cell的大小,而最重要的一步就来了,如何设置UICollectionView的布局呢?,这个就是整个标签组最为重要的一部分。
1.如何实现cell的布局位置要靠左对齐,并实现到屏幕最右边时能自动换行;
2.如何实现每一个不同长度的cell的具体距离和到不会被UICollectionViewFlowLayout默认数据自动拉伸到屏幕平分呢;

这个时候,我们就需要重写UICollectionViewFlowLayout,首先要获取相关容器的宽度,和或者每一个cell的宽度,然后通过计算每一个行cell的宽度和间隙相加的和是否大于容器的宽度,如果大于,即可更换其行数,以下就是 .m文件的直接代码展示(参考来源 @Giovanni Lodi):

@interface UICollectionViewLayoutAttributes (LeftAligned)

- (void)leftAlignFrameWithSectionInset:(UIEdgeInsets)sectionInset;

@end

@implementation UICollectionViewLayoutAttributes (LeftAligned)

- (void)leftAlignFrameWithSectionInset:(UIEdgeInsets)sectionInset
{
    CGRect frame = self.frame;
    frame.origin.x = sectionInset.left;
    self.frame = frame;
}

@end

#pragma mark -

@implementation UICollectionViewLeftAlignedLayout

#pragma mark - UICollectionViewLayout

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray *originalAttributes = [super layoutAttributesForElementsInRect:rect];
    NSMutableArray *updatedAttributes = [NSMutableArray arrayWithArray:originalAttributes];
    for (UICollectionViewLayoutAttributes *attributes in originalAttributes) {
        if (!attributes.representedElementKind) {
            NSUInteger index = [updatedAttributes indexOfObject:attributes];
            updatedAttributes[index] = [self layoutAttributesForItemAtIndexPath:attributes.indexPath];
        }
    }

    return updatedAttributes;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewLayoutAttributes* currentItemAttributes = [[super layoutAttributesForItemAtIndexPath:indexPath] copy];
    UIEdgeInsets sectionInset = [self evaluatedSectionInsetForItemAtIndex:indexPath.section];

    BOOL isFirstItemInSection = indexPath.item == 0;
    CGFloat layoutWidth = CGRectGetWidth(self.collectionView.frame) - sectionInset.left - sectionInset.right;

    if (isFirstItemInSection) {
        [currentItemAttributes leftAlignFrameWithSectionInset:sectionInset];
        return currentItemAttributes;
    }

    NSIndexPath* previousIndexPath = [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];
    CGRect previousFrame = [self layoutAttributesForItemAtIndexPath:previousIndexPath].frame;
    CGFloat previousFrameRightPoint = previousFrame.origin.x + previousFrame.size.width;
    CGRect currentFrame = currentItemAttributes.frame;
    CGRect strecthedCurrentFrame = CGRectMake(sectionInset.left,
                                              currentFrame.origin.y,
                                              layoutWidth,
                                              currentFrame.size.height);
    // if the current frame, once left aligned to the left and stretched to the full collection view
    // width intersects the previous frame then they are on the same line
    BOOL isFirstItemInRow = !CGRectIntersectsRect(previousFrame, strecthedCurrentFrame);

    if (isFirstItemInRow) {
        // make sure the first item on a line is left aligned
        [currentItemAttributes leftAlignFrameWithSectionInset:sectionInset];
        return currentItemAttributes;
    }

    CGRect frame = currentItemAttributes.frame;
    frame.origin.x = previousFrameRightPoint + [self evaluatedMinimumInteritemSpacingForSectionAtIndex:indexPath.section];
    currentItemAttributes.frame = frame;
    return currentItemAttributes;
}

- (CGFloat)evaluatedMinimumInteritemSpacingForSectionAtIndex:(NSInteger)sectionIndex
{
    if ([self.collectionView.delegate respondsToSelector:@selector(collectionView:layout:minimumInteritemSpacingForSectionAtIndex:)]) {
        id<UICollectionViewDelegateLeftAlignedLayout> delegate = (id<UICollectionViewDelegateLeftAlignedLayout>)self.collectionView.delegate;

        return [delegate collectionView:self.collectionView layout:self minimumInteritemSpacingForSectionAtIndex:sectionIndex];
    } else {
        return self.minimumInteritemSpacing;
    }
}

- (UIEdgeInsets)evaluatedSectionInsetForItemAtIndex:(NSInteger)index
{
    if ([self.collectionView.delegate respondsToSelector:@selector(collectionView:layout:insetForSectionAtIndex:)]) {
        id<UICollectionViewDelegateLeftAlignedLayout> delegate = (id<UICollectionViewDelegateLeftAlignedLayout>)self.collectionView.delegate;

        return [delegate collectionView:self.collectionView layout:self insetForSectionAtIndex:index];
    } else {
        return self.sectionInset;
    }
}
@end

那么既然最困难的一步就是布局的问题已经解决了,那整体的页面显示已经完成了,那接下来就是数据的处理了,那要如何实现下图显示?


标签选择.gif

那首先要思考两个问题,如果通过对数据源的设置实现UICollectionView中每一组选中cell或者不选中cell,多选和单选的区别,此时我想到的一个方法就是给数据源一个属性,标识该cell是否选中:

/**
 是否选中
 */
@property (nonatomic, assign) BOOL isSelect;

通过数据源的属性来控制cell内部选中的控件的颜色和状态

    [_tagBtn setTitle:model.name forState: UIControlStateNormal];
    UIColor *containStringColor = model.color == 0 ? tagBlueColor : model.color == 1 ? tagRedColor : model.color == 2 ? tagGreenColor : model.color == 3 ? tagYellowColor : model.color == 4 ? tagVioletColor : tagIndigoColor;
    if (model.isSelect == YES)
    {
        [_tagBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        _tagBtn.backgroundColor = containStringColor;
    }
    else
    {
        [_tagBtn setTitleColor:labelBlackColor forState:UIControlStateNormal];
        _tagBtn.backgroundColor = HEXCOLOR(0xeeeeee);
    }

那controller中的数据该怎么判断是否选中和未选择,然后实现给数据源的属性选中呢?具体思路就是:需要进行遍历数据源中的所有模型进行判断和赋值选中状态,实现方法如下:

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    [collectionView deselectItemAtIndexPath:indexPath animated:NO];
    
    PBCTagGroundModel *groundModel = self.listArrM[indexPath.section];

    DLog(@"indexPath.Seciont = %zd,row = %zd",indexPath.section,indexPath.row);
    
    //替换成可选模式
    for (int i = 0; i < groundModel.tags.count; ++i)
    {
        PBCTagModel *model = groundModel.tags[i];
        
        if (indexPath.row == i)
        {
            if (model.isSelect == YES)
            {
                model.isSelect = NO;
            }
            else
            {
                model.isSelect = YES;
            }
            
            [groundModel.tags replaceObjectAtIndex:i withObject:model];
        }
    }
    [self.listArrM replaceObjectAtIndex:indexPath.section withObject:groundModel];
    [self.collectionView reloadData];
}

既然每一组的cell都实现了选中和未选中状态了,那如何实现判断全选和不选中的状态呢?这里就需要给UICollectionView的头部设置一个点击事件,并且也需要对数据源中的所有数据进行遍历,对数据的选中状态进行选中和未选中的属性赋值。

那么问题来了,如何这是UICollectionView的头部呢?这个和UITableView的操作其他大有雷同:
首先,要先注册UICollectionView的头部:

    //注册头视图
    [collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"UICollectionViewHeader"];

然后实现collectionView的头部尾部设置代理

//设置头视图的大小
-(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section{
    return CGSizeMake([UIScreen mainScreen].bounds.size.width, 44);
}

//创建头视图
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView
           viewForSupplementaryElementOfKind:(NSString *)kind
                                 atIndexPath:(NSIndexPath *)indexPath {
    
    
    NSString *indentifierString = @"UICollectionViewHeader";
    
    UICollectionReusableView *headView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader
                                                                            withReuseIdentifier:indentifierString
                                                                                   forIndexPath:indexPath];
    headView.backgroundColor = [UIColor whiteColor];
    //此处操作是为了防止头部视图重复使用(从缓存机制中取出),导致重叠视图,如要移除此前设置的视图再进行绘制
    for (UIView *subView in headView.subviews)
    {
        [subView removeFromSuperview];
    }

   //设置相关视图
   //.....此处设置
    return headView;
}

设置完头部视图了,那具体的数据操作就是遍历数据修改数据源状态了,代码如下:

/**
 点击选择组便签

 @param btn 组标签
 */
- (void)clickSelectAllBtn:(UIButton *)btn
{
    btn.selected = !btn.selected;
    PBCTagGroundModel *groundModel = self.listArrM[btn.tag];
    
    //替换成可选模式
    for (int i = 0; i < groundModel.tags.count; ++i)
    {
        PBCTagModel *model = groundModel.tags[i];
        if (btn.selected == YES)
        {
            model.isSelect = YES;
        }
        else
        {
            model.isSelect = NO;
        }
        
        [groundModel.tags replaceObjectAtIndex:i withObject:model];
    }
    [self.listArrM replaceObjectAtIndex:btn.tag withObject:groundModel];
    [self.collectionView reloadData];
}

那么对数据源也处理完了,整体的标签组也是实现了。

其实整体设置标签组,最大的难度是在于UICollectionViewFlowLayout的重写和计算,如果这一步解决了,整体的思路很清晰,可以直接解决问题。

以上就是通过UICollectionView来实现标签组的方法,可能实现的路径和方法很多,也有更加多便捷方法和思路,上面方法如有不足之处望大家指出,或者有更优的方法,也欢迎大家来探讨。

类似上图介绍的demo地址:Demo地址

大千世界,求同存异;相遇是缘,相识是份,相知便是“猿粪”(缘分)
From MZou

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

推荐阅读更多精彩内容