I am trying to implement a FlowLayout similar to what you would have in a UICollectionView.
I followed the guide from objc.io but modified it slightly to support custom spacing.
Even prior to my modifications, the issue is present.
Views < width of the layout flow fine. Views that are > the size of the layout do not.
Here is the code behind the layout:
public struct FlowLayout: Layout {
private let spacing: CGFloat
public init(spacing: CGFloat = 8) {
self.spacing = spacing
}
public func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) -> CGSize {
let containerWidth = proposal.replacingUnspecifiedDimensions().width
let sizes = subviews.map {
let dimensions = $0.sizeThatFits(.unspecified)
return CGSize(width: min(dimensions.width, containerWidth), height: dimensions.height)
}
let layoutSizes = layout(sizes: sizes, spacing: spacing, containerWidth: containerWidth)
return layoutSizes.size
}
public func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
let sizes = subviews.map {
let dimensions = $0.sizeThatFits(.unspecified)
return CGSize(width: min(dimensions.width, bounds.width), height: dimensions.height)
}
let offsets = layout(sizes: sizes, spacing: spacing, containerWidth: bounds.width).offsets
for (offset, subview) in zip(offsets, subviews) {
subview.place(
at: CGPoint(x: offset.x + bounds.minX, y: offset.y + bounds.minY),
proposal: .unspecified
)
}
}
private func layout(
sizes: [CGSize],
spacing: CGFloat = 10,
containerWidth: CGFloat
) -> (offsets: [CGPoint], size: CGSize) {
var result: [CGPoint] = []
var currentPosition: CGPoint = .zero
var lineHeight: CGFloat = 0
var maxX: CGFloat = 0
for size in sizes {
if currentPosition.x + size.width > containerWidth {
currentPosition.x = 0
currentPosition.y += lineHeight + spacing
lineHeight = 0
}
result.append(currentPosition)
currentPosition.x += size.width
maxX = max(maxX, currentPosition.x)
currentPosition.x += spacing
lineHeight = max(lineHeight, size.height)
}
return (
result,
CGSize(width: maxX, height: currentPosition.y + lineHeight)
)
}
}
I am expecting the views with text that is larger than the container to expand vertically and wrap its contents.
What am I missing here? VStack and HStack apparently implement this protocol now, and this issue isn't present with them.
Thanks to @BenzyNeez for this suggestion:
The problem was I was using an
.unspecified
proposal when getting the sizes and placing the subviews.The solution was to use a
ProposedViewSize(width:height:)
and specifyingmin(containerWidth, subviewWidth)
for the width, and.infinity
for the height.Here is the updated code: