Custom layout occupies all horizontal space

51 views Asked by At

I have a custom layout that lays views from left to right and if horizontal space runs out then it lays the next views below. If I put a basic text element that says "hello" inside this custom layout then the view occupies all the horizontal space. How can I adjust my setup so the custom layout only occupies the needed horizontal space?

struct ContentView: View {
    var body: some View {
        CustomLayout {
            Text("Hello")
        }
        .background(.blue)
    }
}
struct CustomLayout: Layout {
    var alignment: Alignment = .leading
    var spacing: CGFloat = 0
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let maxWidth = proposal.width ?? 0
        var height: CGFloat = 0
        let rows = generateRows(maxWidth, proposal, subviews)
        
        for (index, row) in rows.enumerated() {
            if index == (rows.count - 1) {
                height += row.maxHeight(proposal)
            } else {
                height += row.maxHeight(proposal) + spacing
            }
        }
        
        return .init(width: maxWidth, height: height)
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        var origin = bounds.origin
        let maxWidth = bounds.width
        
        let rows = generateRows(maxWidth, proposal, subviews)
        
        for row in rows {
            let leading: CGFloat = bounds.maxX - maxWidth
            let trailing = bounds.maxX - (row.reduce(CGFloat.zero) { partialResult, view in
                let width = view.sizeThatFits(proposal).width
                
                if view == row.last {
                    return partialResult + width
                }
                return partialResult + width + spacing
            })
            let center = (trailing + leading) / 2
            
            origin.x = (alignment == .leading ? leading : alignment == .trailing ? trailing : center)
            
            for view in row {
                let viewSize = view.sizeThatFits(proposal)
                view.place(at: origin, proposal: proposal)
                origin.x += (viewSize.width + spacing)
            }
        
            origin.y += (row.maxHeight(proposal) + spacing)
        }
    }
    
    func generateRows(_ maxWidth: CGFloat, _ proposal: ProposedViewSize, _ subviews: Subviews) -> [[LayoutSubviews.Element]] {
        var row: [LayoutSubviews.Element] = []
        var rows: [[LayoutSubviews.Element]] = []
        
        var origin = CGRect.zero.origin
        
        for view in subviews {
            let viewSize = view.sizeThatFits(proposal)
            
            if (origin.x + viewSize.width + spacing) > maxWidth {
                rows.append(row)
                row.removeAll()
                origin.x = 0
                row.append(view)
                origin.x += (viewSize.width + spacing)
            } else {
                row.append(view)
                origin.x += (viewSize.width + spacing)
            }
        }

        if !row.isEmpty {
            rows.append(row)
            row.removeAll()
        }
        
        return rows
    }
}

extension [LayoutSubviews.Element] {
    func maxHeight(_ proposal: ProposedViewSize) -> CGFloat {
        return self.compactMap { view in
            return view.sizeThatFits(proposal).height
        }.max() ?? 0
    }
}
1

There are 1 answers

0
Benzy Neez On BEST ANSWER

The function sizeThatFits needs to compute the width that it really needs to fit inside the proposal it receives. At the moment it is just returning the proposed width it is given, so it is always using the full available width.

For example, you could change the function sizeThatFits to something like this:

func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
    let maxWidth = proposal.width ?? 0
    var width: CGFloat = 0
    var height: CGFloat = 0
    let rows = generateRows(maxWidth, proposal, subviews)

    for (index, row) in rows.enumerated() {
        var rowWidth = CGFloat.zero
        for (i, subview) in row.enumerated() {
            if i > 0 {
                rowWidth += spacing
            }
            rowWidth += subview.sizeThatFits(proposal).width
        }
        width = max(width, rowWidth)
        if index == (rows.count - 1) {
            height += row.maxHeight(proposal)
        } else {
            height += row.maxHeight(proposal) + spacing
        }
    }
    return .init(width: width, height: height)
}

Screenshot