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()
}
}
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) {}
}