SwiftUI HStack with Wrap
Is it possible that the blue tags (which are currently truncated) are displayed completely and then it automatically makes a line break?
NavigationLink(destination: GameListView()) {
VStack(alignment: .leading, spacing: 5){
// Name der Sammlung:
Text(collection.name)
.font(.headline)
// Optional: Für welche Konsolen bzw. Plattformen:
HStack(alignment: .top, spacing: 10){
ForEach(collection.platforms, id: \.self) { platform in
Text(platform)
.padding(.all, 5)
.font(.caption)
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(5)
.lineLimit(1)
}
}
}
.padding(.vertical, 10)
}
Also, there should be no line breaks with in the blue tags:
That's how it should look in the end:
Solution 1:
Here is some approach of how this could be done using alignmentGuide(s). It is simplified to avoid many code post, but hope it is useful.
Update: There is also updated & improved variant of below solution in my answer for SwiftUI HStack with wrap and dynamic height
This is the result:
And here is full demo code (orientation is supported automatically):
import SwiftUI
struct TestWrappedLayout: View {
@State var platforms = ["Ninetendo", "XBox", "PlayStation", "PlayStation 2", "PlayStation 3", "PlayStation 4"]
var body: some View {
GeometryReader { geometry in
self.generateContent(in: geometry)
}
}
private func generateContent(in g: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(self.platforms, id: \.self) { platform in
self.item(for: platform)
.padding([.horizontal, .vertical], 4)
.alignmentGuide(.leading, computeValue: { d in
if (abs(width - d.width) > g.size.width)
{
width = 0
height -= d.height
}
let result = width
if platform == self.platforms.last! {
width = 0 //last item
} else {
width -= d.width
}
return result
})
.alignmentGuide(.top, computeValue: {d in
let result = height
if platform == self.platforms.last! {
height = 0 // last item
}
return result
})
}
}
}
func item(for text: String) -> some View {
Text(text)
.padding(.all, 5)
.font(.body)
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(5)
}
}
struct TestWrappedLayout_Previews: PreviewProvider {
static var previews: some View {
TestWrappedLayout()
}
}
Solution 2:
For me, none of the answers worked. Either because I had different types of elements or because elements around were not being positioned correctly. Therefore, I ended up implementing my own WrappingHStack which can be used in a very similar way to HStack. You can find it at GitHub: WrappingHStack.
Here is an example:
Code:
WrappingHStack {
Text("WrappingHStack")
.padding()
.font(.title)
.border(Color.black)
Text("can handle different element types")
Image(systemName: "scribble")
.font(.title)
.frame(width: 200, height: 20)
.background(Color.purple)
Text("and loop")
.bold()
WrappingHStack(1...20, id:\.self) {
Text("Item: \($0)")
.padding(3)
.background(Rectangle().stroke())
}.frame(minWidth: 250)
}
.padding()
.border(Color.black)
Solution 3:
I've had ago at creating what you need.
Ive used HStack
's in a VStack
.
You pass in a geometryProxy
which is used for determining the maximum row width.
I went with passing this in so it would be usable within a scrollView
I wrapped the SwiftUI Views in a UIHostingController to get a size for each child.
I then loop through the views adding them to the row until it reaches the maximum width, in which case I start adding to a new row.
This is just the init and final stage combining and outputting the rows in the VStack
struct WrappedHStack<Content: View>: View {
private let content: [Content]
private let spacing: CGFloat = 8
private let geometry: GeometryProxy
init(geometry: GeometryProxy, content: [Content]) {
self.content = content
self.geometry = geometry
}
var body: some View {
let rowBuilder = RowBuilder(spacing: spacing,
containerWidth: geometry.size.width)
let rowViews = rowBuilder.generateRows(views: content)
let finalView = ForEach(rowViews.indices) { rowViews[$0] }
VStack(alignment: .center, spacing: 8) {
finalView
}.frame(width: geometry.size.width)
}
}
extension WrappedHStack {
init<Data, ID: Hashable>(geometry: GeometryProxy, @ViewBuilder content: () -> ForEach<Data, ID, Content>) {
let views = content()
self.geometry = geometry
self.content = views.data.map(views.content)
}
init(geometry: GeometryProxy, content: () -> [Content]) {
self.geometry = geometry
self.content = content()
}
}
The magic happens in here
extension WrappedHStack {
struct RowBuilder {
private var spacing: CGFloat
private var containerWidth: CGFloat
init(spacing: CGFloat, containerWidth: CGFloat) {
self.spacing = spacing
self.containerWidth = containerWidth
}
func generateRows<Content: View>(views: [Content]) -> [AnyView] {
var rows = [AnyView]()
var currentRowViews = [AnyView]()
var currentRowWidth: CGFloat = 0
for (view) in views {
let viewWidth = view.getSize().width
if currentRowWidth + viewWidth > containerWidth {
rows.append(createRow(for: currentRowViews))
currentRowViews = []
currentRowWidth = 0
}
currentRowViews.append(view.erasedToAnyView())
currentRowWidth += viewWidth + spacing
}
rows.append(createRow(for: currentRowViews))
return rows
}
private func createRow(for views: [AnyView]) -> AnyView {
HStack(alignment: .center, spacing: spacing) {
ForEach(views.indices) { views[$0] }
}
.erasedToAnyView()
}
}
}
and here's extensions I used
extension View {
func erasedToAnyView() -> AnyView {
AnyView(self)
}
func getSize() -> CGSize {
UIHostingController(rootView: self).view.intrinsicContentSize
}
}
You can see the full code with some examples here: https://gist.github.com/kanesbetas/63e719cb96e644d31bf027194bf4ccdb