Search code examples
iosswiftswiftuilazyvgrid

How to make SwiftUI Grid lay out evenly based on width?


I'm trying to use a SwiftUI Lazy Grid to lay out views with strings of varying lengths. How can I construct my code so that, e.g. if 3 view's do not fit, it will only make 2 columns and push the 3rd view to the next row so that they won't overlap?

struct ContentView: View {
    var data = [
    "Beatles",
    "Pearl Jam",
    "REM",
    "Guns n Roses",
    "Red Hot Chili Peppers",
    "No Doubt",
    "Nirvana",
    "Tom Petty and the Heart Breakers",
    "The Eagles"
   
    ]
    
    var columns: [GridItem] = [
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible())
    ]
    
    
    var body: some View {
        LazyVGrid(columns: columns, alignment: .center) {
            ForEach(data, id: \.self) { bandName in
                Text(bandName)
                    .fixedSize(horizontal: true, vertical: false)
            }
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

enter image description here


Solution

  • Preview

    You can use this method to achieve what you're looking for, solution source: https://www.fivestars.blog/articles/flexible-swiftui/

    Usage

    struct ContentView: View {
    // MARK: - PROPERTIES
    
    var data = [
        "Beatles",
        "Pearl Jam",
        "REM",
        "Guns n Roses",
        "Red Hot Chili Peppers",
        "No Doubt",
        "Nirvana",
        "Tom Petty and the Heart Breakers",
        "The Eagles"
       
        ]
    
    // MARK: - BODY
    
    var body: some View {
        GeometryReader { geometryProxy in
            FlexibleView(
                        availableWidth: geometryProxy.size.width, data: data,
                        spacing: 15,
                        alignment: .leading
                      ) { item in
                        Text(item)
                          .padding(8)
                          .background(
                            RoundedRectangle(cornerRadius: 8)
                              .fill(Color.gray.opacity(0.2))
                           )
                      }
                      .padding(.horizontal, 10)
            }
        }
    
    }
    
    // MARK: - PREVIEW
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    

    FlexibleView

    // MARK: - FLEXIBLE VIEW
    
    struct FlexibleView<Data: Collection, Content: View>: View where Data.Element: Hashable {
    let availableWidth: CGFloat
    let data: Data
    let spacing: CGFloat
    let alignment: HorizontalAlignment
    let content: (Data.Element) -> Content
    @State var elementsSize: [Data.Element: CGSize] = [:]
    
    var body : some View {
        VStack(alignment: alignment, spacing: spacing) {
            ForEach(computeRows(), id: \.self) { rowElements in
                HStack(spacing: spacing) {
                    ForEach(rowElements, id: \.self) { element in
                    content(element)
                            .fixedSize()
                            .readSize { size in
                                elementsSize[element] = size
                            }
                    }
                }
            }
        }
    }
    
    func computeRows() -> [[Data.Element]] {
        var rows: [[Data.Element]] = [[]]
        var currentRow = 0
        var remainingWidth = availableWidth
    
        for element in data {
          let elementSize = elementsSize[element, default: CGSize(width: availableWidth, height: 1)]
    
          if remainingWidth - (elementSize.width + spacing) >= 0 {
            rows[currentRow].append(element)
          } else {
            currentRow = currentRow + 1
            rows.append([element])
            remainingWidth = availableWidth
          }
    
          remainingWidth = remainingWidth - (elementSize.width + spacing)
        }
    
        return rows
    }
    }
    

    View Extension

    // MARK: - EXTENSION
    
    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)
        }
    }
    
    private struct SizePreferenceKey: PreferenceKey {
        static var defaultValue: CGSize = .zero
        static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
    }