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