iOS UICollectionView实现动态标签(单选、多选)
项目中我们经常会遇到标签动态展示的问题,有时我们也需要实现单选或者多选的功能
<1> 针对标签动态展示,我们解决的核心办法就是动态计算文本宽度
即:标签宽度=文本宽度+左右间距
核心代码:
#pragma mark -- UICollectionViewDelegateFlowLayout - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { TaskValueModel *model = self.model.orgRelationCollection[indexPath.row]; NSString *str = ObjErrorCheck(model.value); // 核心代码 动态宽度计算 // LL_ScreenWidth -74 (文本最大宽度)最大就是一行放置一个 CGRect itemFrame = [str boundingRectWithSize:CGSizeMake(LL_ScreenWidth -74, 14) options: NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:14]} context:nil]; // item的宽度 文本宽度+左右间距合计24 CGFloat width = itemFrame.size.width + 24; // item的size return CGSizeMake(width, 32); }
<2> 针对单选或者多选,核心就是在标签模型上的selected标记,然后根据操作改变这个值
单选核心代码
#pragma mark -- UICollectionViewDelegate - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { // 单选功能 TaskValueModel *model = self.model.orgRelationCollection[indexPath.row]; // 选中再次点击取消选中 if (model.selected) { model.selected = !model.selected; // 未选中点击选中,同时确保其它未选中 } else { [self.model.orgRelationCollection enumerateObjectsUsingBlock:^(TaskValueModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if (model.id == obj.id) { obj.selected = YES; }else { obj.selected = NO; } }]; } [self.collectionView reloadData]; }
多选核心代码
#pragma mark -- UICollectionViewDelegate - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { // 多选实现 TaskValueModel *model = self.model.valueTypeCollection[indexPath.row]; model.selected = !model.selected; [self.collectionView reloadData]; }
注意点:
文本的最大宽度 :LL_ScreenWidth -74 (文本最大宽度)最大就是一行放置一个
计算高度时的最大宽度(xOffset + 当前item的宽):LL_ScreenWidth -50 最大宽度(collectionView的宽度)
今天我就简述下使用UICollectionView实现上述功能,
1、自定义布局 LeftAlignFlowLayout
2、自定义标签 ValueTypeCollectionViewCell
3、标签对应的模型 TaskValueModel(因为我们需要在模型中记录选中未选状态,并且会做修改,所以不建议使用NSDictionary,不然修改会比较麻烦)
4、多选功能(未选点击选中,再次点击取消选中)
5、单选功能(未选点击选中且保证其它是未选中,再次点击取消选中)ValueTypeTableViewCell
效果如图:
话不多说,直接上代码
一、自定义布局LeftAlignFlowLayout:
LeftAlignFlowLayout.h
// // LeftAlignFlowLayout.h // CollectionViewDemo // // Created by LJY on 2017/8/24. // Copyright © 2017年 LJY. All rights reserved. // #import <UIKit/UIKit.h> @interface LeftAlignFlowLayout : UICollectionViewFlowLayout /** * 可以通过UICollectionViewFlowLayout的属性,或者UICollectionViewDelegateFlowLayout的方法设置布局属性。皆不设置 * 采用系统默认值。 */ + (instancetype) leftAlignLayoutWithDelegate:(id<UICollectionViewDelegateFlowLayout>) delegate; @end
LeftAlignFlowLayout.m
// // LeftAlignFlowLayout.m // CollectionViewDemo // // Created by LJY on 2017/8/24. // Copyright © 2017年 LJY. All rights reserved. // #import "LeftAlignFlowLayout.h" @interface LeftAlignFlowLayout () @property (nonatomic, weak) id<UICollectionViewDelegateFlowLayout> delegate; @property (nonatomic, strong) NSMutableArray *arrForItemAtrributes; @end @implementation LeftAlignFlowLayout + (instancetype) leftAlignLayoutWithDelegate:(id<UICollectionViewDelegateFlowLayout>) delegate { LeftAlignFlowLayout* layout = [[LeftAlignFlowLayout alloc] init]; layout.delegate = delegate; return layout; } #pragma mark Override method - (void)prepareLayout { [super prepareLayout]; CGFloat xForItemOrigin = self.sectionInsetCustomer.left; CGFloat yForItemOrigin = self.sectionInsetCustomer.top; CGFloat itemHeight = [self itemSizeCustomer:[NSIndexPath indexPathForRow:0 inSection:0]].height; CGFloat xOffset = self.sectionInsetCustomer.left; NSUInteger numberOfItems = [self.collectionView numberOfItemsInSection:0]; NSUInteger numberOfRows = 0; self.arrForItemAtrributes = [NSMutableArray arrayWithCapacity:numberOfItems]; // 为每个item确定LayoutAttribute属性,同时将这些属性放入布局数组中 for(int i = 0; i <numberOfItems; i++) { NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0]; UICollectionViewLayoutAttributes *layoutArr = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; CGFloat itemWidth = [self itemSizeCustomer:indexPath].width;; //xOffset + 当前item的宽 <= 最大宽度 if ((xOffset + itemWidth) <= (self.collectionView.bounds.size.width - self.sectionInsetCustomer.right - self.sectionInsetCustomer.left)) { //设置x xForItemOrigin = xOffset; xOffset += itemWidth + self.minimumInteritemSpacingCustomer; } else { //换行 xForItemOrigin = self.sectionInsetCustomer.left; xOffset = self.sectionInsetCustomer.left + itemWidth + self.minimumInteritemSpacingCustomer; numberOfRows++; } // 设置y yForItemOrigin = self.sectionInsetCustomer.top + (itemHeight+ self.minimumLineSpacingCustomer) * numberOfRows; layoutArr.frame = CGRectMake(xForItemOrigin, yForItemOrigin, itemWidth, itemHeight); [self.arrForItemAtrributes addObject:layoutArr]; } } - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { return self.arrForItemAtrributes; } #pragma mark - Inner method /** * default 10 */ - (CGFloat)minimumLineSpacingCustomer { if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView:layout:minimumLineSpacingForSectionAtIndex:)]) { return [self.delegate collectionView:self.collectionView layout:self minimumLineSpacingForSectionAtIndex:0]; } else { return self.minimumLineSpacing; } } /** * default 10 */ - (CGFloat)minimumInteritemSpacingCustomer { if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView:layout:minimumInteritemSpacingForSectionAtIndex:)]) { return [self.delegate collectionView:self.collectionView layout:self minimumInteritemSpacingForSectionAtIndex:0]; } else { return self.minimumInteritemSpacing; } } /** * default UIEdgeInsetsZero */ - (UIEdgeInsets)sectionInsetCustomer { if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView:layout:insetForSectionAtIndex:)]) { return [self.delegate collectionView:self.collectionView layout:self insetForSectionAtIndex:0]; } else { return self.sectionInset; } } /** * default {50, 50} */ - (CGSize)itemSizeCustomer:(NSIndexPath *)indexPath { if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView:layout:sizeForItemAtIndexPath:)]) { return [self.delegate collectionView:self.collectionView layout:self sizeForItemAtIndexPath:indexPath]; } else { return self.itemSize; } } /** * default CGSizeZero */ - (CGSize)headerReferenceSizeCustomer { if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView:layout:referenceSizeForHeaderInSection:)]) { return [self.delegate collectionView:self.collectionView layout:self referenceSizeForHeaderInSection:0]; } else { return self.headerReferenceSize; } } /** * default CGSizeZero */ - (CGSize)footerReferenceSizeCustomer { if (self.delegate && [self.delegate respondsToSelector:@selector(collectionView:layout:referenceSizeForFooterInSection:)]) { return [self.delegate collectionView:self.collectionView layout:self referenceSizeForFooterInSection:0]; } else { return self.footerReferenceSize; } } @end
二、自定义标签ValueTypeCollectionViewCell
ValueTypeCollectionViewCell.h
#import <UIKit/UIKit.h> #import "iCCarrerCircleTaskDetailsNewModel.h" NS_ASSUME_NONNULL_BEGIN static NSString *const ValueTypeCollectionViewCellReused = @"ValueTypeCollectionViewCell"; @interface ValueTypeCollectionViewCell : UICollectionViewCell - (void)configCell:(TaskValueModel *)model; @end
ValueTypeCollectionViewCell.m
#import "ValueTypeCollectionViewCell.h" @interface ValueTypeCollectionViewCell () @property (nonatomic, strong) UIView *bgView; @property (nonatomic, strong) UILabel *tagLabel; @property (nonatomic, strong) UIImageView *rightIcon; @end @implementation ValueTypeCollectionViewCell #pragma mark - System - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self initData]; [self initUI]; } return self; } #pragma mark - Init Data - (void)initData { } #pragma mark - Init UI - (void)initUI { self.backgroundColor = [UIColor clearColor]; [self.contentView addSubview:self.bgView]; [self.bgView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.contentView); }]; [self.bgView addSubview:self.rightIcon]; [self.rightIcon mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.bgView); make.top.equalTo(self.bgView); make.width.height.mas_equalTo(19); }]; [self.bgView addSubview:self.tagLabel]; [self.tagLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.bgView); }]; } - (UIView *)bgView { if (!_bgView) { _bgView = [[UIView alloc] init]; [_bgView cornerAngel:5]; } return _bgView; } - (UILabel *)tagLabel { if(!_tagLabel){ _tagLabel = [[UILabel alloc]init]; _tagLabel.font = FF_PFR_ICOME(14); } return _tagLabel; } - (UIImageView *)rightIcon { if(!_rightIcon){ _rightIcon = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"task_value_sel"]]; } return _rightIcon; } #pragma mark - Reload View - (void)configCell:(TaskValueModel *)model { self.tagLabel.text = ObjErrorCheck(model.value); if (model.allowdClick) { if (model.selected) { // 选中样式 self.bgView.backgroundColor = CC_ICOME(@"#3480FF1A"); [self.bgView borderWidth:ONE_PIXEL andBorderColor:CC_ICOME(@"#3480FF33")]; self.tagLabel.textColor = CC_ICOME(@"#3480FF"); self.rightIcon.hidden = NO; } else { // 未选中样式 self.bgView.backgroundColor = XZWL_COLOR_FFFFFF; [self.bgView borderWidth:ONE_PIXEL andBorderColor:CC_ICOME(@"#BBBBBB")]; self.tagLabel.textColor = CC_ICOME(@"#666666"); self.rightIcon.hidden = YES; } } else { // 不可点击样式 self.bgView.backgroundColor = CC_ICOME(@"#3480FF1A"); [self.bgView borderWidth:ONE_PIXEL andBorderColor:CC_ICOME(@"#3480FF33")]; self.tagLabel.textColor = CC_ICOME(@"#3480FF"); self.rightIcon.hidden = YES; } } @end
三、标签对应模型TaskValueModel
TaskValueModel.h
@protocol TaskValueModel @end @interface TaskValueModel : JSONModel @property (nonatomic, copy) NSString *value;// @property (nonatomic, assign) NSInteger id;// @property (nonatomic, assign) BOOL selected;// @property (nonatomic, assign) BOOL allowdClick;// @end
四、单选功能卡片ValueTypeTableViewCell
ValueTypeTableViewCell.h
#import "BaseTableViewCell.h" #import "iCCarrerCircleTaskDetailsNewModel.h" NS_ASSUME_NONNULL_BEGIN @interface ValueTypeTableViewCell : BaseTableViewCell + (CGFloat)getCellHeight:(iCCarrerCircleTaskDetailsNewModel *)model; - (void)configCell:(iCCarrerCircleTaskDetailsNewModel *)model; @end
ValueTypeTableViewCell.m
#import "ValueTypeTableViewCell.h" #import "ValueTypeCollectionViewCell.h" #import "LeftAlignFlowLayout.h" @interface ValueTypeTableViewCell ()<UICollectionViewDelegateFlowLayout,UICollectionViewDelegate, UICollectionViewDataSource> @property (nonatomic, strong) UIView *bgView; @property (nonatomic, strong) UIImageView *LImage; @property (nonatomic, strong) UILabel *LLab; @property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) iCCarrerCircleTaskDetailsNewModel *model; @end @implementation ValueTypeTableViewCell #pragma mark - System - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { [self initUI]; } return self; } #pragma mark - Init Data - (void)initData { } #pragma mark - Init UI - (void)initUI { self.accessoryType = UITableViewCellAccessoryNone; self.selectionStyle = UITableViewCellSelectionStyleNone; self.backgroundColor = [UIColor clearColor]; [self.contentView addSubview:self.bgView]; [self.bgView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.contentView.mas_top); make.left.equalTo(self.contentView.mas_left).offset(10); make.right.equalTo(self.contentView.mas_right).offset(-10); make.bottom.equalTo(self.contentView); }]; [self.bgView addSubview:self.LImage]; [self.LImage mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.bgView).offset(15); make.top.equalTo(self.bgView).offset(17); make.height.mas_equalTo(17); make.width.mas_equalTo(16); }]; [self.bgView addSubview:self.LLab]; [self.LLab mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.bgView).offset(44); make.centerY.equalTo(self.LImage); }]; [self.bgView addSubview:self.collectionView]; [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.bgView).offset(15); make.top.equalTo(self.bgView).offset(52); make.bottom.equalTo(self.bgView).offset(-20); make.right.equalTo(self.bgView).offset(-15); }]; } - (UIView *)bgView { if (!_bgView) { _bgView = [[UIView alloc] init]; _bgView.backgroundColor = XZWL_COLOR_FFFFFF; [_bgView cornerAngel:8]; } return _bgView; } - (UILabel *)LLab { if (!_LLab) { _LLab = [[UILabel alloc]init]; _LLab.font = FF_PFM_ICOME(16); _LLab.textColor = XZWL_COLOR_333333; _LLab.text = @"单选测试"; } return _LLab; } - (UIImageView *)LImage { if (!_LImage) { _LImage = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"task_org"]]; } return _LImage; } - (UICollectionView *)collectionView { if (!_collectionView) { // 核心代码 自定义布局LeftAlignFlowLayout LeftAlignFlowLayout *layout = [LeftAlignFlowLayout leftAlignLayoutWithDelegate:self]; layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; _collectionView.delegate = self; _collectionView.dataSource = self; _collectionView.showsHorizontalScrollIndicator = NO; _collectionView.showsVerticalScrollIndicator = NO; _collectionView.backgroundColor = [UIColor clearColor]; [_collectionView registerClass:[ValueTypeCollectionViewCell class] forCellWithReuseIdentifier:ValueTypeCollectionViewCellReused]; if (@available(iOS 11.0, *)) { _collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } else { self.viewController.automaticallyAdjustsScrollViewInsets = NO; } } return _collectionView; } #pragma mark - Event #pragma mark - DataSource #pragma mark -- UICollectionViewDataSource - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { return 1; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return self.model.orgRelationCollection.count; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { ValueTypeCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:ValueTypeCollectionViewCellReused forIndexPath:indexPath]; TaskValueModel *model = self.model.orgRelationCollection[indexPath.row]; [cell configCell:model]; return cell; } #pragma mark - Delegate #pragma mark -- UICollectionViewDelegate - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { // 单选功能 TaskValueModel *model = self.model.orgRelationCollection[indexPath.row]; // 选中再次点击取消选中 if (model.selected) { model.selected = !model.selected; // 未选中点击选中,同时确保其它未选中 } else { [self.model.orgRelationCollection enumerateObjectsUsingBlock:^(TaskValueModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if (model.id == obj.id) { obj.selected = YES; }else { obj.selected = NO; } }]; } [self.collectionView reloadData]; } #pragma mark -- UICollectionViewDelegateFlowLayout - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { TaskValueModel *model = self.model.orgRelationCollection[indexPath.row]; NSString *str = ObjErrorCheck(model.value); // 核心代码 动态宽度计算 // LL_ScreenWidth -74 (文本的最大宽度)最大就是一行放置一个 CGRect itemFrame = [str boundingRectWithSize:CGSizeMake(LL_ScreenWidth -74, 14) options: NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:14]} context:nil]; // item的宽度 文本宽度+左右间距合计24 CGFloat width = itemFrame.size.width + 24; // item的size return CGSizeMake(width, 32); } #pragma mark - Reload View + (CGFloat)getCellHeight:(iCCarrerCircleTaskDetailsNewModel *)model { CGFloat heightmp = 0; // 核心代码 高度计算 if (model.orgRelationCollection.count) { // itemSizes 存放itemsize的集合 NSMutableArray *itemSizes = [NSMutableArray array]; for (TaskValueModel *modelt in model.orgRelationCollection) { CGRect itemFrame = [ObjErrorCheck(modelt.value) boundingRectWithSize:CGSizeMake(LL_ScreenWidth -74, 14) options: NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:14]} context:nil]; CGFloat width = itemFrame.size.width + 24; NSValue *itemSize =[NSValue valueWithCGSize:CGSizeMake(width, 32)]; [itemSizes addObject:itemSize]; } NSInteger row = 1; CGFloat xOffset = 0 ; // 单个item高度 CGFloat height = 32; for(int i = 0; i <itemSizes.count; i++) { CGFloat itemWidth = [itemSizes[i] CGSizeValue].width; //xOffset + 当前item的宽 <= 最大宽度(collectionView的宽度) if ((xOffset + itemWidth) <= (LL_ScreenWidth - 50)) { xOffset += itemWidth + 10; } else { //换行 xOffset = itemWidth + 10; row++; } } // collectionview高度: 行数 * 行高 + (行数 - 1)*行间距 CGFloat maxHeight = row * height + (row - 1) * 10 ; // 总高度: collectionview高度 + 上下间距 heightmp = maxHeight + 72; } return heightmp; } - (void)configCell:(iCCarrerCircleTaskDetailsNewModel *)model{ self.model = model; self.collectionView.userInteractionEnabled = NO; if (self.model.orgRelationCollection.count) { TaskValueModel *modelt=self.model.orgRelationCollection[0]; if (modelt.allowdClick) { self.collectionView.userInteractionEnabled = YES; } self.hidden = NO; [self.collectionView reloadData]; } else { self.hidden = YES; } } @end
五、多选功能与单选相似,只是处理选中逻辑不同
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { // 多选实现 TaskValueModel *model = self.model.valueTypeCollection[indexPath.row]; model.selected = !model.selected; [self.collectionView reloadData]; }