1

I've implemented a left aligned flow layout using the new iOS 16 Layout protocol, and I'm using it to add two lists of items to a ScrollView as follows:

let people = ["Albert", "Bernard", "Clarence", "Desmond", "Ethelbert", "Frederick", "Graeme", "Hortense", "Inigo"]
let places = ["Adelaide", "Birmingham", "Chester", "Dar es Salaam", "East Lothian"]

struct ContentView: View {
    
    var body: some View {
        ScrollView(.vertical) {
            LeftAlignedFlowLayout {
                ForEach(people, id: \.self) { name in
                    NameView(name: name, colour: .red)
                }
            }
            LeftAlignedFlowLayout {
                ForEach(places, id: \.self) { name in
                    NameView(name: name, colour: .green)
                }
            }
        }
        .padding()
    }
}


struct NameView: View {
    let name: String
    let colour: Color
    
    var body: some View {
        Text(name)
            .font(.body)
            .padding(.vertical, 6)
            .padding(.horizontal, 12)
            .background(Capsule().fill(colour))
            .foregroundColor(.black)
    }
}

struct LeftAlignedFlowLayout: Layout {
    
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let height = calculateRects(width: proposal.width ?? 0, subviews: subviews).last?.maxY ?? 0
        return CGSize(width: proposal.width ?? 0, height: height)
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        
        calculateRects(width: bounds.width, subviews: subviews).enumerated().forEach { index, rect in
            let sizeProposal = ProposedViewSize(rect.size)
            subviews[index].place(at: rect.origin, proposal: sizeProposal)
        }
    }
    
    func calculateRects(width: CGFloat, subviews: Subviews) -> [CGRect] {
        
        var nextPosition = CGPoint.zero
        return subviews.indices.map { index in
            
            let size = subviews[index].sizeThatFits(.unspecified)
            
            var nextHSpacing: CGFloat = 0
            var previousVSpacing: CGFloat = 0
            
            if index > subviews.startIndex {
                let previousIndex = index.advanced(by: -1)
                previousVSpacing = subviews[previousIndex].spacing.distance(to: subviews[index].spacing, along: .vertical)
            }
            
            if index < subviews.endIndex.advanced(by: -1) {
                let nextIndex = index.advanced(by: 1)
                nextHSpacing = subviews[index].spacing.distance(to: subviews[nextIndex].spacing, along: .horizontal)
            }
            
            if nextPosition.x + nextHSpacing + size.width > width {
                nextPosition.x = 0
                nextPosition.y += size.height + previousVSpacing
            }
            
            let thisPosition = nextPosition
            print(thisPosition)
            nextPosition.x += nextHSpacing + size.width
            return CGRect(origin: thisPosition, size: size)
        }
    }
}

The LeftAlignedFlowLayout works as expected, returning the correct heights and positioning the subviews correctly, but the two layouts are overlapping:

enter image description here

I've tried embedding the two LeftAlignedFlowLayout in a VStack, with the same result.

If I add another View between the two layouts, e.g.

LeftAlignedFlowLayout {
...           
}
Text("Hello")
LeftAlignedFlowLayout {
...
}

I get the following result:

enter image description here

which seems to show that the correct size is being returned for the layout.

Any thoughts as to how to resolve this issue?

2 Answers 2

1

Your calculateRects() is always starting the layout at CGPoint.zero when it should be starting at bounds.origin. Since calculateRects() doesn't have access to the bounds, pass the desired starting origin as an additional parameter to calculateRects(). In sizeThatFits(), just pass CGPoint.zero as the origin, and in placeSubviews(), pass bounds.origin as the origin:

struct LeftAlignedFlowLayout: Layout {
    
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let height = calculateRects(origin: CGPoint.zero, width: proposal.width ?? 0, subviews: subviews).last?.maxY ?? 0
        return CGSize(width: proposal.width ?? 0, height: height)
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        
        calculateRects(origin: bounds.origin, width: bounds.width, subviews: subviews).enumerated().forEach { index, rect in
            let sizeProposal = ProposedViewSize(rect.size)
            subviews[index].place(at: rect.origin, proposal: sizeProposal)
        }
    }
    
    func calculateRects(origin: CGPoint, width: CGFloat, subviews: Subviews) -> [CGRect] {
        
        var nextPosition = origin // was CGPoint.zero
        return subviews.indices.map { index in
            
            let size = subviews[index].sizeThatFits(.unspecified)
            
            var nextHSpacing: CGFloat = 0
            var previousVSpacing: CGFloat = 0
            
            if index > subviews.startIndex {
                let previousIndex = index.advanced(by: -1)
                previousVSpacing = subviews[previousIndex].spacing.distance(to: subviews[index].spacing, along: .vertical)
            }
            
            if index < subviews.endIndex.advanced(by: -1) {
                let nextIndex = index.advanced(by: 1)
                nextHSpacing = subviews[index].spacing.distance(to: subviews[nextIndex].spacing, along: .horizontal)
            }
            
            if nextPosition.x + nextHSpacing + size.width > width {
                nextPosition.x = 0
                nextPosition.y += size.height + previousVSpacing
            }
            
            let thisPosition = nextPosition
            print(thisPosition)
            nextPosition.x += nextHSpacing + size.width
            return CGRect(origin: thisPosition, size: size)
        }
    }
}
Sign up to request clarification or add additional context in comments.

Comments

1

OK, problem solved. The issue was this:

var nextPosition = CGPoint.zero

when what it needs to be is:

var nextPosition = bounds.origin

2 Comments

I came to the same conclusion. I solved it by passing the bounds.origin to caculateRects: func calculateRects(origin: CGPoint, width: CGFloat, subviews: Subviews) -> [CGRect] {. Since bounds is not in the scope of calculateRects, how did you get it there?
Exactly the same way you did; passing origin: CGPoint into calculateRects, with .zero in sizeThatFits. Great minds think alike! Thank you for your answer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.