I created this package because my previous company needed to replace an old third-party tag view library, so after discussion we decided to write one for own using. Since the project was still based on UIKit architecture but I wanted to practice SwiftUI, I created this tag view package that works seamlessly with both frameworks.
Originally built for iOS 16, I have now upgraded it to use the new Layout API for iOS 17+. (2026/1/22)
Note: There are known stability issues with some UIKit components, especially when embedding
tag list viewinsideUITableViewCell, which may cause layout glitches. Please double-check if you plan to use it this way. However, if your page is written in pure SwiftUI and bridged to UIKit usingUIHostingController, you shouldn't encounter these issues (maybe
MNTagView is a modern Tag View framework built on the power of SwiftUI, designed specifically for iOS 17+. It leverages the latest Layout protocol to deliver high-performance flow layouts while providing a highly unified and easy-to-use API for both SwiftUI and UIKit.
- ⚡️ Built for iOS 17+: Utilizes SwiftUI's modern
Layoutprotocol for superior performance and stable flow layouts. - 📱 Seamless Cross-Platform Support:
- SwiftUI: Supports native
Bindingfor reactive data flow, adhering to the "Single Source of Truth" principle. - UIKit: Provides a complete property facade, making it feel just like a native
UIView.
- SwiftUI: Supports native
- 🎨 Highly Customizable:
- Customizable corner radius, borders, font size, and font name.
- Supports single background colors and text colors.
- Flexible padding configuration using
MNEdgeInsetsfor cross-platform compatibility.
- 🛠 Flexible Layout:
- Supports Vertical, Horizontal scrolling, or None (auto-expanding).
- Supports Leading, Center, and Trailing alignment.
- 👆 Interactive: Built-in support for tap selection and remove buttons.
- Business Logic: When the remove button is enabled (Edit Mode), the tap selection is automatically disabled to allow users to focus on editing.
- 💾 Custom Data Support: Each tag can carry extra information (Metadata) using the
metaDataproperty with type-safe generic accessors.
In Xcode, select File > Add Packages... and enter the following URL:
https://github.com/michaelnamara0326/MNTagView.git
Note: This package requires iOS 17.0 or later.
MNTagView offers a SwiftUI-idiomatic API with Binding support for automatic data synchronization.
import SwiftUI
import MNTagView
struct ContentView: View {
@State private var tags = ["SwiftUI", "iOS", "Apple", "WWDC"]
var body: some View {
TagListViewSwiftUI(tags: $tags)
// 1. Layout Configuration
.scrollAxis(.vertical)
.alignment(.leading)
.spacing(10)
// 2. Styling
.tagCornerRadius(12)
.tagBackgroundColor(.blue)
.tagTextColor(.white)
.viewPadding(MNEdgeInsets(16)) // Unified MNEdgeInsets
// 3. Interactions
.onTagPressed { tag in
// This won't trigger if .tagRemoveButtonEnable(true)
print("Pressed: \(tag.model.title)")
tag.model.isSelected.toggle()
}
.onRemoveTag { tag in
// Data is automatically removed from the 'tags' array when using Binding
print("Removed: \(tag.model.title)")
}
}
}In UIKit, you no longer need to deal with complex ViewModels. We provide a direct property interface.
import UIKit
import MNTagView
class ViewController: UIViewController {
let tagView = TagListViewUIKit()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tagView)
tagView.frame = CGRect(x: 0, y: 100, width: 300, height: 200)
// 1. Set Data
tagView.tags = ["UIKit", "Interface", "Legacy", "Modern"]
// 2. Set Delegate
tagView.delegate = self
// 3. Styling (Direct properties)
tagView.scrollAxis = .vertical
tagView.alignment = .leading
tagView.spacing = 8
tagView.tagBackgroundColor = .systemBlue
tagView.textColor = .white
tagView.cornerRadius = 8
// Cross-platform padding
tagView.tagPadding = MNEdgeInsets(horizontal: 12, vertical: 6)
// Enable Remove Button (tagPressed delegate will be disabled)
tagView.isRemoveButtonEnabled = true
}
}
extension ViewController: TagViewDelegate {
func tagPressed(_ tag: TagSubView) {
print("Tag selected: \(tag.model.title)")
tag.model.isSelected.toggle()
}
func removeButtonPressed(_ tag: TagSubView) {
print("Remove requested for: \(tag.model.title)")
// In UIKit, you must manually call remove to update the UI if managing data manually
tagView.removeTagView(tag: tag)
}
}To manage styles centrally, you can use MNTagConfig to apply multiple settings at once:
var config = MNTagConfig()
config.cornerRadius = 20
config.textSize = 14
config.textColor = .white
config.tagBackgroundColor = .systemPurple
config.tagPadding = MNEdgeInsets(10) // Unified padding
// SwiftUI
TagListViewSwiftUI(tags: $tags)
.setConfig(config)
// UIKit
tagView.setConfig(config)To unify padding configuration across both platforms, we introduced MNEdgeInsets. It converts seamlessly to SwiftUI's EdgeInsets or UIKit's UIEdgeInsets.
// Uniform
let insets = MNEdgeInsets(10)
// Horizontal / Vertical
let insets = MNEdgeInsets(horizontal: 16, vertical: 8)
// Custom
let insets = MNEdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10)Each tag can carry custom metadata. You can retrieve it in a type-safe way using the .data<T>() method:
// 1. Set custom data (can be any type)
let tag = TagSubView(title: "Apple", metaData: Product(id: 99, price: 50))
// 2. Retrieve data type-safely
if let product: Product = tag.model.data() {
print("Product ID: \(product.id)")
}This project includes a comprehensive Demo App (in the TagViewDemo folder) showcasing:
- Complete implementation examples for both SwiftUI and UIKit.
- Interactive Control Panel: Real-time adjustment of alignment, spacing, corner radius, colors, etc.
- Pinned Preview: See changes instantly as you tweak settings.
MNTagView is released under the MIT license. See LICENSE for details.