3

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

enter image description here

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

enter image description here

1 Answers1

2

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