Browse Questions
Use GeometryReader to get the real container width, alignmentGuide to position items, and a PreferenceKey to measure and report total height:
struct WrapHStack<Data: RandomAccessCollection, Content: View, ID: Hashable>: View {
let items: Data
let idKey: KeyPath<Data.Element, ID>
let content: (Data.Element) -> Content
@State private var totalHeight = CGFloat.zero
var body: some View {
GeometryReader { geometry in
self.generateContent(in: geometry)
}
.frame(height: totalHeight)
}
private func generateContent(in geometry: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
let rowSpacing: CGFloat = 5
let maxWidth = geometry.size.width
return ZStack(alignment: .topLeading) {
ForEach(items, id: idKey) { item in
content(item)
.padding(.horizontal, 4)
.alignmentGuide(.leading) { d in
if abs(width - d.width) > maxWidth {
width = 0
height -= d.height + rowSpacing
}
let result = width
if item[keyPath: idKey] == items.last?[keyPath: idKey] { width = 0 }
else { width -= d.width }
return result
}
.alignmentGuide(.top) { _ in
let result = height
if item[keyPath: idKey] == items.last?[keyPath: idKey] { height = 0 }
return result
}
}
}
.background(viewHeightReader($totalHeight))
}
private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
GeometryReader { geometry in
Color.clear
.preference(key: ViewHeightKey.self, value: geometry.size.height)
}
.onPreferenceChange(ViewHeightKey.self) { binding.wrappedValue = $0 }
}
}
Limitations: requires RandomAccessCollection input (no inline content), row spacing is hardcoded to 5, adds .padding(.horizontal, 4) to every item.