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:
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:
which seems to show that the correct size is being returned for the layout.
Any thoughts as to how to resolve this issue?
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)
}
}
}