Last active
April 17, 2025 18:21
-
-
Save TAATHub/5965b7b8bcf4457ce05102cd14f39ca2 to your computer and use it in GitHub Desktop.
Responsive Chips Selection
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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