Skip to content

Instantly share code, notes, and snippets.

@shtnkgm
Created December 29, 2023 21:14
Show Gist options
  • Save shtnkgm/1b78a25bbd071631fb3f148655c9f0cf to your computer and use it in GitHub Desktop.
Save shtnkgm/1b78a25bbd071631fb3f148655c9f0cf to your computer and use it in GitHub Desktop.
import SwiftUI
struct TagListView<Data: Collection, Content: View>: View where Data.Element: Hashable {
let items: Data
let itemSpacing: CGFloat
let lineSpacing: CGFloat
let alignment: HorizontalAlignment
let content: (Data.Element) -> Content
@State private var width: CGFloat = 0
var body: some View {
ZStack(alignment: Alignment(horizontal: alignment, vertical: .center)) {
Color.clear
.frame(height: 1)
.readSize { size in
width = size.width
}
_TagListView(
width: width,
items: items,
itemSpacing: itemSpacing,
lineSpacing: lineSpacing,
alignment: alignment,
content: content
)
}
}
}
struct _TagListView<Data: Collection, Content: View>: View where Data.Element: Hashable {
let width: CGFloat
let items: Data
let itemSpacing: CGFloat
let lineSpacing: CGFloat
let alignment: HorizontalAlignment
let content: (Data.Element) -> Content
@State var itemsSize: [Data.Element: CGSize] = [:]
var body: some View {
VStack(alignment: alignment, spacing: lineSpacing) {
ForEach(rows, id: \.self) { rowItems in
HStack(spacing: itemSpacing) {
ForEach(rowItems, id: \.self) { item in
content(item)
.fixedSize()
.readSize { size in
itemsSize[item] = size
}
}
}
}
}
}
var rows: [[Data.Element]] {
var result: [[Data.Element]] = [[]]
var currentRow = 0
var remainingWidth = width
items.forEach { item in
let itemWidth = (itemsSize[item] ?? CGSize(width: width, height: 1)).width
if remainingWidth >= itemWidth {
result[currentRow].append(item)
} else {
currentRow += 1
result.append([item])
remainingWidth = width
}
remainingWidth -= itemWidth + itemSpacing
}
return result
}
}
extension View {
func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
func readFrame(in coordinateSpace: CoordinateSpace, onChange: @escaping (CGRect) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: RectPreferenceKey.self, value: geometryProxy.frame(in: coordinateSpace))
}
)
.onPreferenceChange(RectPreferenceKey.self, perform: onChange)
}
}
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
private struct RectPreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment