Skip to content

Instantly share code, notes, and snippets.

@TAATHub
Last active April 17, 2025 18:21
Show Gist options
  • Save TAATHub/5965b7b8bcf4457ce05102cd14f39ca2 to your computer and use it in GitHub Desktop.
Save TAATHub/5965b7b8bcf4457ce05102cd14f39ca2 to your computer and use it in GitHub Desktop.
Responsive Chips Selection
// Responsive Chips Selection
// See Also: https://youtu.be/T82izB2XBMA?si=Cnf6rGdyisG8ZWoX
import SwiftUI
struct Tag: Hashable {
var name: String
var color: Color
}
// Generated by AI
let tags: [Tag] = [
Tag(name: "Swift", color: .orange),
Tag(name: "Python", color: .blue),
Tag(name: "JavaScript", color: .yellow),
Tag(name: "Kotlin", color: .purple),
Tag(name: "Rust", color: .brown),
Tag(name: "Go", color: .cyan),
Tag(name: "C++", color: .gray),
Tag(name: "Java", color: .red),
Tag(name: "Ruby", color: .pink),
Tag(name: "TypeScript", color: .indigo)
]
struct ContentView: View {
var body: some View {
VStack {
ChipsView(tags: tags) { tag, isSelected in
ChipView(tag: tag, isSelected: isSelected)
} didChangeSelection: { selection in
print("Selected tags: \(selection.map { $0.name })")
}
.padding(16)
.background(.gray.opacity(0.1), in: .rect(cornerRadius: 16))
}
.padding(16)
}
}
struct ChipView: View {
var tag: Tag
var isSelected: Bool
var body: some View {
HStack(spacing: 4) {
if isSelected {
Image(systemName: "checkmark")
.font(.callout)
.foregroundStyle(.white)
}
Text(tag.name)
.font(.callout)
.foregroundStyle(isSelected ? .white : tag.color)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background {
Capsule()
.fill(isSelected ? tag.color : .white)
.stroke(tag.color, style: .init(lineWidth: 1))
}
}
}
struct ChipsView<Content: View, Tag: Equatable>: View where Tag: Hashable {
var spacing: CGFloat = 10
var tags: [Tag]
@ViewBuilder var content: (Tag, Bool) -> Content
var didChangeSelection: ([Tag]) -> ()
@State private var selectedTags: [Tag] = []
var body: some View {
ChipLayout(spacing: spacing) {
ForEach(tags, id: \.self) { tag in
content(tag, selectedTags.contains(tag))
.contentShape(.rect)
.onTapGesture {
withAnimation(.easeInOut(duration: 0.2)) {
if selectedTags.contains(tag) {
selectedTags.removeAll(where: { $0 == tag })
} else {
selectedTags.append(tag)
}
}
didChangeSelection(selectedTags)
}
}
}
}
}
struct ChipLayout: Layout {
var spacing: CGFloat
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let width = proposal.width ?? 0
return .init(width: width, height: maxHeight(proposal: proposal, subviews: subviews))
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
var origin = bounds.origin
for subview in subviews {
let fitSize = subview.sizeThatFits(proposal)
if (origin.x + fitSize.width) > bounds.maxX {
// If subview is about to exceed bounds.maxX, move the origin to a new line
origin.x = bounds.minX
origin.y += fitSize.height + spacing
}
subview.place(at: origin, proposal: proposal)
origin.x += fitSize.width + spacing
}
}
private func maxHeight(proposal: ProposedViewSize, subviews: Subviews) -> CGFloat {
var origin: CGPoint = .zero
for subview in subviews {
let fitSize = subview.sizeThatFits(proposal)
if (origin.x + fitSize.width) > (proposal.width ?? 0) {
// If subview is about to exceed proposal.width, move the origin to a new line
origin.x = 0
origin.y += fitSize.height + spacing
}
origin.x += fitSize.width + spacing
if subview == subviews.last {
origin.y += fitSize.height
}
}
return origin.y
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment