swiftswiftui

How can I send grid item to next row if there isn't enough space swiftui


I'm somewhat new to swift and am struggling with this seemingly simple problem. I want three columns in my grid UNLESS the content of the grid item doesn't fit within the set width of the column. Specifically, I have filter buttons and some of which have long names. I want to send the TagView of "Breakfast" to the next line and move each one along so that they all fit and the text doesn't have to wrap. Not sure how to do this though.

I've tried adjusting the minimum and maximum values within the grid items and adjusted how many GridItems are in the columns. However, this hasn't worked unfortunately. Any advice would be much appreciated.

Here is my code:

    var filterBar: some View {
        VStack {
            filterBarTitle
            if filtersOpened {
                let columns = [GridItem(.adaptive(minimum: 85), spacing:0)]
                
                LazyVGrid(columns: columns) {
                    ForEach(allRecipeTags) { recipeTag in
                        TagView(tag: recipeTag, selected: selectedTags.contains(recipeTag), canToggle: true, onTap: {
                            if selectedTags.contains(recipeTag) {
                                print("removing \(recipeTag)")
                                selectedTags.removeAll(where: { $0 == recipeTag })
                            } else {
                                print("adding \(recipeTag)")
                                selectedTags.append(recipeTag)
                            }
                        })
                        .border(.red)
                    }
                }
                .border(.blue)
            }
        }
        .padding([.top, .bottom])
        .border(width: 1, edges: [.bottom], color: Color("24292F").opacity(0.3))
        .padding(.horizontal)
        .padding([.bottom], filtersOpened ? 8 : 0)
    }

FilterBar Image


Solution

  • Here is a simple implementation of a custom Layout, which hopefully suits your purpose:

    enter image description here

    struct ContentView: View {
        
        let tags = ["Appetizers","Baking","Breakfast","Dessert","Dinner","Fish","Lunch","Main","Quick","Side","Snack","Spicy","Vegan","Vegetarian"]
        
        var body: some View {
    
            FlowLayout {
                    ForEach(tags, id: \.self) { tag in
                        
                        HStack {
                            Text(tag)
                                .fixedSize()
                                .font(.headline)
    
                            Circle()
                        }
                        .padding(10)
                                .background(
                                    Capsule().stroke(.red)
                                )
                                .padding(5)
                    }
            }
        }
    }
    
    
    struct FlowLayout: Layout {
        
        func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
            let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
    
            var totalHeight: CGFloat = 0
            var totalWidth: CGFloat = 0
    
            var lineWidth: CGFloat = 0
            var lineHeight: CGFloat = 0
    
            for size in sizes {
                if lineWidth + size.width > proposal.width ?? 0 {
                    totalHeight += lineHeight
                    lineWidth = size.width
                    lineHeight = size.height
                } else {
                    lineWidth += size.width
                    lineHeight = max(lineHeight, size.height)
                }
    
                totalWidth = max(totalWidth, lineWidth)
            }
    
            totalHeight += lineHeight
    
            return .init(width: totalWidth, height: totalHeight)
        }
    
        func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
            let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
    
            var lineX = bounds.minX
            var lineY = bounds.minY
            var lineHeight: CGFloat = 0
    
            for index in subviews.indices {
                if lineX + sizes[index].width > (proposal.width ?? 0) {
                    lineY += lineHeight
                    lineHeight = 0
                    lineX = bounds.minX
                }
    
                subviews[index].place(
                    at: .init(
                        x: lineX + sizes[index].width / 2,
                        y: lineY + sizes[index].height / 2
                    ),
                    anchor: .center,
                    proposal: ProposedViewSize(sizes[index])
                )
    
                lineHeight = max(lineHeight, sizes[index].height)
                lineX += sizes[index].width
            }
        }
    }