Good day. I am trying to release dynamic placement of elements based on their size. Works in an empty project, but sometimes does not show the entire text. And in a working project, a maximum of 2-3 elements are displayed. Maybe I'm on the wrong path. Please tell me in which direction to look. I thought to use UICollectionView and UIViewRepresentable but couldn't figure out exactly how. There were examples on the Internet, but there was a common grid and the columns had the same width. And I just need to pave elements of different widths all the space from left to right line by line.
struct CustomFlexBoxView<Content> : View where Content: View {
let alignment: Alignment
let spacing: CGFloat
let content: [Content]
@State private var sizeBody: CGSize? = nil
@State private var sizeItems: [Int:CGSize] = [:]
init(alignment: Alignment = .center, spacing: CGFloat = 0, content: [Content]) {
self.spacing = spacing
self.alignment = alignment
self.content = content
}
var body: some View {
GeometryReader { (geo) in
if let sizeBody = self.sizeBody {
self.contentView(sizeBody: sizeBody)
}
else {
self.contentFirstView
.onAppear {
self.sizeBody = geo.frame(in: .global).size
}
}
}
}
private var contentFirstView: some View {
let items = self.content
return VStack(spacing: 0) {
ForEach(0 ..< items.count) { (index) in
HStack(spacing: 0) {
items[index]
}
.background(
GeometryReader { (geo) in
Color.clear.onAppear {
self.sizeItems[index] = geo.frame(in: .global).size
}
}
)
}
}
}
private func contentView(sizeBody: CGSize) -> some View {
let items = self.content
var rowWidth: CGFloat = 0
var rowItems: [Content] = []
var rows: [AnyView] = []
for index in 0 ..< items.count {
if let size = self.sizeItems[index] {
if rowWidth + size.width + self.spacing <= sizeBody.width {
let addSpacing = (rowItems.isEmpty ? 0 : self.spacing)
rowItems.append(items[index])
rowWidth = rowWidth + size.width + addSpacing
}
else {
if rowItems.isEmpty == false {
rows.append(
AnyView(
self.createRow(items: rowItems)
)
)
rowWidth = 0
rowItems = []
}
rowWidth = size.width
rowItems = [ items[index] ]
}
}
else {
if rowItems.isEmpty == false {
rows.append(
AnyView(
self.createRow(items: rowItems)
)
)
rowWidth = 0
rowItems = []
}
rows.append(AnyView(items[index]))
}
}
if rowItems.isEmpty == false {
rows.append(
AnyView(
self.createRow(items: rowItems)
)
)
rowWidth = 0
rowItems = []
}
return AnyView (
VStack(alignment: self.alignment.horizontal, spacing: self.spacing) {
ForEach(0 ..< rows.count) { ind in
rows[ind]
}
}
)
}
private func createRow(items: [Content]) -> some View {
HStack(alignment: self.alignment.vertical, spacing: self.spacing) { [items] in
ForEach(0 ..< items.count) { ind in
items[ind]
}
}
}
}
On an empty project everything works, in the working one the first 2-3 elements are displayed.:
struct ContentView: View {
@State var data: [Int] = [
113, 2, 2342343, 234, 234234234234324, 3,
45345435345345, 545, 34, 4, 345345345, 45345, 5, 5
]
var body: some View {
ScrollView {
CustomFlexBoxView(
alignment: .topLeading,
spacing: 10,
content: self.data.map { TestText(text: "\($0)") }
)
}
.padding()
}
}
struct TestText : View {
let text: String
@State private var color: Bool = false
var body: some View {
Text(self.text)
.lineLimit(1)
.fixedSize()
.background(self.color ? Color.orange : Color.gray)
.foregroundColor(self.color ? Color.green : Color.black)
.onTapGesture {
self.color.toggle()
}
}
}

But if you add more elements, then for some reason the total size of the CustomFlexBoxView is not estimated correctly:
var body: some View {
ScrollView {
Rectangle()
.fill(Color.orange)
.frame(maxWidth: .infinity, minHeight: 100)
CustomFlexBoxView(
alignment: .topLeading,
spacing: 10,
content: self.data.map { TestText(text: "\($0)") }
)
Rectangle()
.fill(Color.orange)
.frame(maxWidth: .infinity, minHeight: 100)
}
.padding()
}

I made solutions for my project, but I'm not sure if this will always work.
struct CustomFlexBoxView<Item, Content> : View where Item: Hashable, Content: View {
let alignment: Alignment
let spacing: CGFloat
let items: [Item]
let content: (Int) -> Content
@State private var sizeBody: CGSize? = nil
@State private var widthItems: [Item: CGFloat] = [:]
init(alignment: Alignment = .center, spacing: CGFloat = 0, items: [Item], @ViewBuilder content: @escaping (Int) -> Content) {
self.spacing = spacing
self.alignment = alignment
self.items = items
self.content = content
}
var body: some View {
self.contentView
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: self.alignment)
.background(
GeometryReader { (geo) in
Color.clear.onAppear {
self.sizeBody = geo.frame(in: .global).size
}
}
)
}
private var contentView: some View {
VStack(alignment: self.alignment.horizontal, spacing: self.spacing) {
ForEach(self.rowsIndices, id: \.self) { (row) in
self.createRow(indices: row)
}
}
}
private func createRow(indices: [Int]) -> some View {
HStack(alignment: self.alignment.vertical, spacing: self.spacing) {
ForEach(indices, id: \.self) { (index) in
Group {
self.content(index)
}
.background(
GeometryReader { (geo) in
Color.clear.onAppear {
self.widthItems[self.items[index]] = geo.frame(in: .global).size.width
}
}
)
}
}
}
private var rowsIndices: [[Int]] {
guard let widthBody = self.sizeBody?.width else {
return self.items.indices.map { [ $0 ] }
}
var rowWidth: CGFloat = 0
var rowItems: [Int] = []
var rows: [[Int]] = []
for index in 0 ..< items.count {
if let widthItem = self.widthItems[self.items[index]] {
let rowWidthNext = rowWidth + widthItem + (rowItems.isEmpty ? 0 : self.spacing)
if rowWidthNext <= widthBody {
rowItems.append(index)
rowWidth = rowWidthNext
}
else {
if rowItems.isEmpty == false {
rows.append(rowItems)
rowWidth = 0
rowItems = []
}
rowWidth = widthItem
rowItems = [ index ]
}
}
else {
if rowItems.isEmpty == false {
rows.append(rowItems)
rowWidth = 0
rowItems = []
}
rows.append([ index ])
}
}
if rowItems.isEmpty == false {
rows.append(rowItems)
rowWidth = 0
rowItems = []
}
return rows
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With