TTGTagCollectionView
June 14, 2026 · View on GitHub
TTGTagCollectionView is a Swift-first iOS tag layout component. Use it for filter chips, topic labels, search facets, dense table cells, custom tag views, and any UI that needs predictable wrapping or horizontal tag rows.

Highlights
- Text tags or custom views:
TextTagCollectionViewfor styled text,TagCollectionViewfor arbitraryUIViewcontent. - Flexible layout: vertical wrapping, horizontal scrolling, line limits, spacing, insets, and 6 alignment modes.
- Per-tag appearance: gradient backgrounds, borders, shadows, corner radius, padding, size constraints, and selected states.
- AutoLayout friendly: intrinsic size updates and
preferredMaxLayoutWidthfor stack views, forms, and self-sizing cells. - Cache-aware performance: text measurement cache, pure layout cache, and precomputed content-size APIs for dense lists.
- Swift and Objective-C: modern Swift sources with Objective-C-compatible names and selectors.
Requirements
- iOS 16.0+
- Swift 5.9+
- Xcode 15+
Installation
Swift Package Manager
In Xcode, choose File -> Add Package Dependencies and enter:
https://github.com/zekunyan/TTGTagCollectionView.git
Or add it to Package.swift:
dependencies: [
.package(url: "https://github.com/zekunyan/TTGTagCollectionView.git", from: "3.0.0")
]
CocoaPods
pod 'TTGTagCollectionView'
Quick Start
The screenshots below are generated from the Swift example app running in an iOS Simulator.
1. Create the view

import TTGTags
let tagView = TextTagCollectionView()
tagView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tagView)
NSLayoutConstraint.activate([
tagView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
tagView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
tagView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24)
])
2. Build content plus style

Each TextTag is built from content and style. The same model also carries selection state, accessibility metadata, and optional attachment data.
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()
3. Add selected states

selectedStyle is applied automatically when a tag is selected. Use selectionLimit and the delegate callbacks for app-specific behavior.
let selectedStyle = TextTagStyle()
selectedStyle.backgroundColor = .systemOrange
selectedStyle.cornerRadius = 12
selectedStyle.extraSpace = CGSize(width: 14, height: 8)
tag.selectedStyle = selectedStyle
tagView.selectionLimit = 3
tagView.delegate = self
4. Control layout behavior

Use the same view for normal wrapping tag clouds, fill-width rows, or horizontal filter bars.
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()
Objective-C
#import <TTGTags/TTGTags-Swift.h>
TTGTextTagCollectionView *tagView = [[TTGTextTagCollectionView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:tagView];
TTGTextTagStringContent *content = [TTGTextTagStringContent contentWithText:@"Hello"];
TTGTextTagStyle *style = [TTGTextTagStyle new];
style.backgroundColor = UIColor.systemBlueColor;
style.cornerRadius = 10;
style.extraSpace = CGSizeMake(12, 8);
TTGTextTag *tag = [TTGTextTag tagWithContent:content style:style];
[tagView addTag:tag];
[tagView reload];
Concepts
Understanding these 4 building blocks will help you use the library effectively:

| Concept | Class | Role |
|---|---|---|
| Tag | TextTag | Data model: holds content, style, selection state, and an optional attachment |
| Content | TextTagStringContent / TextTagAttributedStringContent | What text to display, with font and color |
| Style | TextTagStyle | Visual appearance: background, gradient, corners, border, shadow, size |
| Collection View | TextTagCollectionView / TagCollectionView | Container that lays out tags with alignment, spacing, and scroll |
Key rule: always call
reload()after adding, removing, or updating tags.
Visual Assets
- Promo poster: Resources/promo_poster.png
- Quick Start images: Resources/quick_start_01_create.png, Resources/quick_start_02_style.png, Resources/quick_start_03_selection.png, Resources/quick_start_04_layout.png
- Concepts image: Resources/concepts_poster.png
- Review HTML: Resources/promo_poster.html, Resources/quick_start_01_create.html, Resources/quick_start_02_style.html, Resources/quick_start_03_selection.html, Resources/quick_start_04_layout.html
- Regenerate PNG assets with
node Resources/render_readme_images.mjs.
Architecture at a Glance

The architecture poster is generated from Resources/architecture_poster.html. It summarizes the Swift-first architecture, Objective-C compatibility layer, pure layout engine, rendering flow, and the cache-aware path used for dense tag lists.
Usage
Delegate
tagView.delegate = self
// TextTagCollectionViewDelegate
func textTagCollectionView(_ collectionView: TextTagCollectionView,
canTapTag tag: TextTag, at index: Int) -> Bool { true }
func textTagCollectionView(_ collectionView: TextTagCollectionView,
didTapTag tag: TextTag, at index: Int) {
print("tapped: \(tag.rightfulContent.contentAttributedString.string), selected: \(tag.selected)")
}
func textTagCollectionView(_ collectionView: TextTagCollectionView,
updateContentSize contentSize: CGSize) {
// e.g. update a height constraint
}
Content types
// Plain text
let c1 = TextTagStringContent(text: "Hello")
c1.textFont = .systemFont(ofSize: 14)
c1.textColor = .darkText
// Rich text via NSAttributedString
let attrs: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.systemRed,
.font: UIFont.boldSystemFont(ofSize: 16)
]
let c2 = TextTagAttributedStringContent(
attributedText: NSAttributedString(string: "Rich", attributes: attrs)
)
Style properties — TextTagStyle
let style = TextTagStyle()
// Background
style.backgroundColor = .systemBlue
// Gradient background
style.enableGradientBackground = true
style.gradientBackgroundStartColor = .systemBlue
style.gradientBackgroundEndColor = .systemPurple
style.gradientBackgroundStartPoint = CGPoint(x: 0, y: 0.5)
style.gradientBackgroundEndPoint = CGPoint(x: 1, y: 0.5)
// Corner (all corners by default; set individual flags for per-corner control)
style.cornerRadius = 14
style.cornerTopLeft = true
style.cornerTopRight = true
style.cornerBottomLeft = false
style.cornerBottomRight = false
// Border
style.borderWidth = 1
style.borderColor = .white
// Shadow
style.shadowColor = .black
style.shadowOffset = CGSize(width: 2, height: 2)
style.shadowRadius = 2
style.shadowOpacity = 0.3
// Size
style.extraSpace = CGSize(width: 12, height: 6) // padding
style.minWidth = 60 // 0 = no limit
style.maxWidth = 200
style.exactWidth = 0 // 0 = auto
style.exactHeight = 32
Layout configuration
tagView.scrollDirection = .vertical // .vertical (default) or .horizontal
tagView.alignment = .left // see Alignment below
tagView.numberOfLines = 0 // 0 = unlimited
tagView.selectionLimit = 3 // 0 = unlimited
tagView.horizontalSpacing = 8
tagView.verticalSpacing = 8
tagView.contentInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
// AutoLayout manual height
tagView.manualCalculateHeight = true
tagView.preferredMaxLayoutWidth = 320
// Tap callbacks (no delegate needed)
tagView.onTapBlankArea = { point in print("tapped blank at \(point)") }
tagView.onTapAllArea = { point in print("tapped anywhere at \(point)") }
Alignment modes
| Swift | Description |
|---|---|
.left | Left-aligned (default) |
.center | Center-aligned |
.right | Right-aligned |
.fillByExpandingSpace | Expand spacing between tags to fill each row |
.fillByExpandingWidth | Expand each tag's width to fill each row |
.fillByExpandingWidthExceptLastLine | Same as above but skip the last row |
Tag model — TextTag
let tag = TextTag()
// Content & style for normal / selected state
tag.content = TextTagStringContent(text: "Label")
tag.style = TextTagStyle()
tag.selectedContent = TextTagStringContent(text: "Selected") // optional, falls back to content copy
tag.selectedStyle = TextTagStyle() // optional, falls back to style copy
// Selection
tag.selected = false
tag.onSelectStateChanged = { selected in print(selected) }
// Attach any object
tag.attachment = myModel
// Accessibility
tag.enableAutoDetectAccessibility = true // auto sets label + traits from content
// or manually:
tag.isAccessibilityElement = true
tag.accessibilityLabel = "My tag"
tag.accessibilityHint = "Double tap to select"
tag.accessibilityTraits = .button
// Current active content / style (respects selected state)
let activeContent = tag.rightfulContent
let activeStyle = tag.rightfulStyle
Mutating tags
// Add
tagView.add(tag: tag)
tagView.add(tags: [tag1, tag2])
// Insert
tagView.insert(tag: tag, at: 0)
tagView.insert(tags: [tag1, tag2], at: 2)
// Update
tagView.updateTag(at: 0, selected: true)
tagView.updateTag(at: 0, with: newTag)
// Remove
tagView.remove(tag: tag)
tagView.removeTag(byId: tag.tagId)
tagView.removeTag(at: 0)
tagView.removeAllTags()
// Query
let tag = tagView.getTag(at: 0)
let tags = tagView.getTags(in: NSRange(location: 0, length: 3))
let all = tagView.allTags()
let selected = tagView.allSelectedTags()
let unselected = tagView.allNotSelectedTags()
// Reload (required after any mutation)
tagView.reload()
Hit-testing
let index = tagView.indexOfTag(at: touchPoint) // NSNotFound if missed
TagCollectionView — custom views
Use TagCollectionView when your tags are arbitrary UIView subclasses instead of text.
tagCollectionView.dataSource = self
tagCollectionView.delegate = self
// TagCollectionViewDataSource
func numberOfTags(in tagCollectionView: TagCollectionView) -> Int { items.count }
func tagCollectionView(_ tagCollectionView: TagCollectionView,
tagViewFor index: Int) -> UIView { myViews[index] }
// TagCollectionViewDelegate
func tagCollectionView(_ tagCollectionView: TagCollectionView,
sizeForTagAt index: Int) -> CGSize { myViews[index].frame.size }
func tagCollectionView(_ tagCollectionView: TagCollectionView,
didSelectTag tagView: UIView, at index: Int) {
print("selected \(index)")
}
func tagCollectionView(_ tagCollectionView: TagCollectionView,
updateContentSize contentSize: CGSize) { }
// Reload
tagCollectionView.reload()
All layout and spacing properties (scrollDirection, alignment, numberOfLines, horizontalSpacing, verticalSpacing, contentInset, manualCalculateHeight, preferredMaxLayoutWidth) are identical to TextTagCollectionView.
Performance
TTGTagCollectionView 3.0 keeps layout deterministic and cache-friendly:
TagCollectionLayoutis a pure calculator: the same tag sizes and configuration produce the same frames and content size.TextTagCollectionViewcaches text tag measurements by attributed content, style constraints, selected state, and available width.- Layout results are cached for repeated tag size arrays and collection view configuration.
TextTagCollectionView.contentSize(for:width:...)lets table/list screens precompute row height without creating or laying out a cell.- Demo table cells batch tag configuration and call
reload()once per reuse pass.
Dense tags inside UITableViewCell
For smooth scrolling, build tag models once, cache the row height by table width, and configure the cell in one pass:
let tagSize = TextTagCollectionView.contentSize(
for: tags,
width: availableTagWidth,
scrollDirection: .vertical,
alignment: .fillByExpandingWidth,
numberOfLines: 0,
horizontalSpacing: 8,
verticalSpacing: 8,
contentInset: UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0)
)
let rowHeight = titleHeight + tagSize.height + verticalPadding
Objective-C uses the same API through TTGTags-Swift.h:
CGSize tagSize = [TTGTextTagCollectionView contentSizeForTags:tags
width:availableTagWidth
scrollDirection:TTGTagCollectionScrollDirectionVertical
alignment:TTGTagCollectionAlignmentFillByExpandingWidth
numberOfLines:0
horizontalSpacing:8
verticalSpacing:8
contentInset:UIEdgeInsetsMake(4, 0, 4, 0)];
When data changes globally or you need to benchmark a cold path, clear both caches:
TextTagCollectionView.clearMeasurementCache()
Tips
- Always call
reload()after adding, removing, or updating tags. - When embedding in a
UITableViewCell, prefer precomputing row height withTextTagCollectionView.contentSize(for:width:...)and reusing the result by table width. - Configure all tags first, then call
reload()once. Avoid repeatedupdateTag(...)calls during cell reuse. - Use
manualCalculateHeight = true+preferredMaxLayoutWidthwhen the view's width is not yet determined at layout time.
Source Structure
Sources/TTGTags/
├── Model/
│ ├── TextTag.swift # Tag data model (id, content, style, selection)
│ ├── TextTagContent.swift # Abstract content base class
│ ├── TextTagStringContent.swift # Plain text content
│ └── TextTagAttributedStringContent.swift # NSAttributedString content
├── Style/
│ └── TextTagStyle.swift # Visual style (background, border, shadow, corner, size)
├── Layout/
│ └── TagCollectionLayout.swift # Pure layout calculator (no UIKit side effects)
└── View/
├── TagCollectionView.swift # Custom-view tag collection
├── TextTagCollectionView.swift # Text tag collection
└── Internal/
├── TextTagComponentView.swift # Per-tag rendering view
└── TextTagGradientLabel.swift # CAGradientLayer-backed label
Tests/TTGTagsTests/
├── TagCollectionLayoutTests.swift
├── TextTagTests.swift
└── TextTagContentTests.swift
3.0 Migration Guide
Version 3.0 rewrites all core sources in Swift. Objective-C class names, selectors, and enum cases are fully preserved via @objc(TTGXxx) aliases — existing OC call sites need only one change: replace the old .h imports with the Swift generated umbrella header.
One-line OC migration:
// Before (2.x)
#import <TTGTags/TTGTextTagCollectionView.h>
// After (3.0+)
#import <TTGTags/TTGTags-Swift.h>
Swift API mapping
| 2.x / Objective-C | 3.0 Swift |
|---|---|
TTGTagCollectionView | TagCollectionView |
TTGTextTagCollectionView | TextTagCollectionView |
TTGTextTag | TextTag |
TTGTextTagStyle | TextTagStyle |
TTGTextTagContent | TextTagContent |
TTGTextTagStringContent | TextTagStringContent |
TTGTextTagAttributedStringContent | TextTagAttributedStringContent |
TTGTagCollectionAlignment | TagCollectionAlignment |
TTGTagCollectionScrollDirection | TagCollectionScrollDirection |
[tag getRightfulContent] | tag.rightfulContent |
[tag getRightfulStyle] | tag.rightfulStyle |
[content getContentAttributedString] | content.contentAttributedString |
[tagView addTag:] | tagView.add(tag:) |
[tagView insertTag:atIndex:] | tagView.insert(tag:at:) |
[tagView removeTagAtIndex:] | tagView.removeTag(at:) |
[tagView getTagAtIndex:] | tagView.getTag(at:) |
[tagView updateTagAtIndex:selected:] | tagView.updateTag(at:selected:) |
Other changes in 3.0
- Minimum deployment target raised from iOS 11 to iOS 16
tagIdauto-increment is now thread-safe (NSLock)- Per-corner
UIRectCornerbitmask bug fixed - Pure layout calculator
TagCollectionLayoutextracted (fully unit-tested) - Text measurement and layout caches added for dense tag lists
TextTagCollectionView.contentSize(for:width:...)added for precomputing list row height- SPM test target added (
Tests/TTGTagsTests)
Author
zekunyan — zekunyan@163.com
License
TTGTagCollectionView is available under the MIT license. See the LICENSE file for more info.