Lab 072 – More fun with HoneycombLayout
Using rotation3DLayout, adjust angle, and animating some changes.
This lab builds on the HoneycombLayout that we started in Lab 067. For the most part, I’m just playing with some animations to change the size, angle, and rotation of the layout. One notable change is that this version will do a better job of expanding the HoneycombLayout content to fill the available space.
This custom layout always adds new elements start from the center. It might be fun to work on a version that works more like a hex grid that can draw from an outer corner. For example, Top Leading or Bottom Trailing.
Video Demo:
Lab Code
Lab 072 Usage
HoneycombLayout(angleOffset: angleOffset, hexSize: hexSize) {
ForEach(0..<nodes, id: \.self) { index in
ModelViewEmoji(
name: "UISphere01",
hexSize: hexSize, emoji: emoji[index],
bundle: realityKitContentBundle
)
.rotation3DLayout(Rotation3D(angle: .degrees(360 - layoutRotation), axis: .x))
}
}Revised HoneycombLayout
fileprivate struct HoneycombLayout: Layout, Animatable {
var angleOffset: Angle = .zero
var hexSize: CGFloat = 60
// Calculate the number of rings needed for a given number of items
private func calculateRings(for itemCount: Int) -> Int {
if itemCount <= 1 { return 1 }
if itemCount <= 7 { return 2 }
if itemCount <= 19 { return 3 }
if itemCount <= 37 { return 4 }
if itemCount <= 61 { return 5 }
return 6 // Default for larger counts
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let updatedProposal = proposal.replacingUnspecifiedDimensions()
let minDim = min(updatedProposal.width, updatedProposal.height)
return CGSize(width: minDim, height: minDim)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
guard !subviews.isEmpty else { return }
let center = CGPoint(x: bounds.midX, y: bounds.midY)
// Calculate dynamic hex size based on available space and number of items
let minDimension = min(bounds.width, bounds.height)
let rings = calculateRings(for: subviews.count)
let availableRadius = minDimension / 2 - 20 // Leave 20pt padding
let dynamicHexSize = min(hexSize, availableRadius / CGFloat(rings))
// Calculate hexagon spacing to fill the available space
let hexSpacing = availableRadius / CGFloat(rings)
// Generate hexagon positions in a spiral pattern
var positions: [CGPoint] = []
positions.append(center) // Center hexagon
var ring = 1
while positions.count < subviews.count {
// For each ring, place hexagons at 60-degree intervals
for i in 0..<6 {
let angle = Double(i) * .pi / 3 + angleOffset.radians
let radius = Double(ring) * hexSpacing
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
positions.append(CGPoint(x: x, y: y))
}
// Fill in the gaps between the corners for larger rings
if ring > 1 {
for i in 0..<6 {
let startAngle = Double(i) * .pi / 3 + angleOffset.radians
let endAngle = Double(i + 1) * .pi / 3 + angleOffset.radians
let radius = Double(ring) * hexSpacing
// Add intermediate positions with adjusted radius for tighter honeycomb
for j in 1..<ring {
let angle = startAngle + (endAngle - startAngle) * Double(j) / Double(ring)
// Adjust radius for items that should be closer to center
// Items at the edges of each segment get pulled in slightly
let radiusAdjustment = 0.15 // Pull items in by 15%
let adjustedRadius = radius * (1.0 - radiusAdjustment)
let x = center.x + adjustedRadius * cos(angle)
let y = center.y + adjustedRadius * sin(angle)
positions.append(CGPoint(x: x, y: y))
}
}
}
ring += 1
}
// Place subviews at calculated positions
for (index, subview) in subviews.enumerated() {
if index < positions.count {
subview.place(
at: positions[index],
anchor: .center,
proposal: .init(width: dynamicHexSize, height: dynamicHexSize)
)
}
}
}
var animatableData: Angle.AnimatableData {
get { angleOffset.animatableData }
set { angleOffset.animatableData = newValue }
}
}Support our work so we can continue to bring you new examples and articles.

Follow Step Into Vision