前言
TTGTagCollectionView 是我很早之前写的标签布局控件。最开始的目标很简单:项目里总是要做“标签云”、“筛选项”、“兴趣标签”这种 UI,不想每次都重新计算换行和选中状态,所以把这块抽出来做了一个库。
这次整理到了 3.3.0,变化就不只是补几个属性了,而是把它重新整理成了一个 Swift-first、Objective-C 仍然可用、布局和视图分离 的版本。简单来说,老项目能继续用,新项目也可以更自然地接入 Swift / SPM / AutoLayout。

Github: https://github.com/zekunyan/TTGTagCollectionView
更新
这次 3.3.0 我自己比较关心的是下面这些点:
- Swift-first:核心源码迁移到 Swift,Package product 为
TTGTags,同时保留 Objective-C 兼容命名。 - 模型化的文字标签:
TextTag拆成 content、style、selectedStyle、attachment,外部更容易管理状态。 - 更强的布局能力:垂直换行、水平滚动、多行限制、6 种 alignment、水平分布和垂直对齐都集中到了布局层。
- AutoLayout 友好:支持
intrinsicContentSize、preferredMaxLayoutWidth,放到 StackView、自适应 Cell、表单里更省心。 - 性能更可控:文字测量缓存和纯布局缓存拆出来,密集标签列表不用每次都全量重新算。
- 交互补齐:除了点击和选中,还加了滑动选择、长按拖拽排序、拖到删除区域等常见需求。

快速使用
现在的使用方式更像“先构造一个 tag 模型,再交给 collection view 布局”:
import TTGTags
let tagView = TextTagCollectionView()
tagView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tagView)
let content = TextTagStringContent(text: "Swift")
content.textFont = .boldSystemFont(ofSize: 14)
content.textColor = .white
let style = TextTagStyle()
style.enableGradientBackground = true
style.gradientBackgroundStartColor = .systemBlue
style.gradientBackgroundEndColor = .systemPurple
style.cornerRadius = 12
style.extraSpace = CGSize(width: 14, height: 8)
let tag = TextTag(content: content, style: style)
tagView.add(tag: tag)
tagView.reload()

如果只做简单文字标签,这个模型看起来比以前多了一层,但好处是后面扩展不会把 TextTagCollectionView 自己撑得越来越大。文字、样式、选中样式、附件数据各管各的,调用方也更容易做 diff、缓存或者复用。
样式更新
以前标签样式基本是一些直接挂在 View 上的属性。现在统一放到了 TextTagStyle:
let selectedStyle = TextTagStyle()
selectedStyle.backgroundColor = .systemOrange
selectedStyle.cornerRadius = 12
selectedStyle.extraSpace = CGSize(width: 14, height: 8)
tag.selectedStyle = selectedStyle
tagView.selectionLimit = 3
TextTagStyle 里面包含背景色、渐变背景、圆角、边框、阴影、padding、最大/最小/固定宽高等属性。这个设计的重点不是属性数量,而是 每个 Tag 可以有自己的 style,不再只能让整个 collection 共享一套样式。

布局层拆出来
这次重构里比较重要的一点,是把布局计算抽成了 TagCollectionLayout。它接收 tag sizes、滚动方向、alignment、spacing、inset、容器宽高等输入,然后输出每个 tag 的 frame 和最终 contentSize。
这样做有几个好处:
- 视图层只负责把 frame 应用到子 view 上,代码职责更清楚。
- 布局计算可以单独写单元测试,不需要依赖 UIKit 交互过程。
- 对相同输入可以做布局缓存,避免频繁刷新时重复计算。

比如下面这个配置,可以从普通换行标签切到水平滚动的筛选条:
tagView.alignment = .fillByExpandingWidth
tagView.horizontalSpacing = 8
tagView.verticalSpacing = 8
tagView.contentInset = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
tagView.scrollDirection = .horizontal
tagView.numberOfLines = 2
tagView.showsHorizontalScrollIndicator = false
tagView.reload()
交互能力
标签控件经常一开始只是展示,后来就会慢慢长出“多选”、“滑动选择”、“排序”、“删除”之类的需求。3.3.0 这次把这些常用交互也补齐了一些:
tagView.enableTagSelection = true
tagView.enableSwipeSelection = true
tagView.enableTagReordering = true
tagView.enableDragToDelete = true
tagView.selectionLimit = 3
对应的 delegate 里可以拦截点击、滑动选择、移动、删除:
func textTagCollectionView(_ collectionView: TextTagCollectionView,
canTapTag tag: TextTag,
at index: Int) -> Bool {
true
}
func textTagCollectionView(_ collectionView: TextTagCollectionView,
didMoveTag tag: TextTag,
fromIndex: Int,
toIndex: Int) {
print("move", fromIndex, toIndex)
}
这类功能放在库里并不是为了替业务层做决定,而是把手势、快照、自动滚动、frame 重排这些重复工作处理掉。业务层只需要决定“能不能选”、“能不能删”、“移动后数据源怎么更新”。
架构

从架构上看,新的版本大致分成几块:
TextTag/TextTagContent/TextTagStyle:描述标签数据和外观。TextTagCollectionView:文字标签的高层封装,处理交互和状态。TagCollectionView:通用自定义 View 标签容器。TagCollectionLayout:纯布局计算。TextTagComponentView:负责最终渲染,包括渐变、圆角、边框、阴影等。
这个拆法比以前多了几个类型,但每个类型的职责更窄。后续如果继续加 SwiftUI 包装、自定义内容、测试用例,也不会都堆到一个类里。
总结
TTGTagCollectionView 3.3.0 更像是一次“把老项目重新整理成现代 iOS 组件”的更新。它保留了最初“简单显示标签”的入口,也补上了 Swift、SPM、AutoLayout、缓存、交互和 Objective-C 兼容这些实际项目里会遇到的问题。
如果项目里只是几个固定标签,自己写当然也可以;但如果标签数量多、样式多、还要放在 Cell 或筛选页里反复复用,用一个专门的布局控件会省掉不少琐碎工作。嗯,这也是这个库一开始存在的原因。