SwiftUI Custom Circular Layout
Custom layouts in SwiftUI are provided using the protocol `Layout`. The `Layout` protocol requires to implement two methods `func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize` and `func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ())`.
In the example below the actual layout calculation places the subviews in a circular position with equal angular space between them.
import SwiftUI struct SampleCircleLayoutView: View { struct CircleLayout: Layout { func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { return proposal.replacingUnspecifiedDimensions() } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { let radius = min(bounds.size.width, bounds.size.height) / 3.0 let angle = Angle.degrees(360.0 / Double(subviews.count)).radians let offset = 0.0 for (index, subview) in subviews.enumerated() { var point = CGPoint(x: 0, y: -radius) .applying(CGAffineTransform( rotationAngle: angle * Double(index) + offset)) point.x += bounds.midX point.y += bounds.midY subview.place(at: point, anchor: .center, proposal: .unspecified) } } } @State private var count: Int = 3 var body: some View { VStack(spacing: 32) { Text("Circle Layout") .font(.largeTitle) HStack { Button(action: { withAnimation(.spring(duration: 0.5, bounce: 0.5)) { count = max(1, count - 1) } }, label: { Image(systemName: "minus.circle.fill") }) .font(.largeTitle) Button(action: { withAnimation(.spring(duration: 0.5, bounce: 0.5)) { count = min(10, count + 1) } }, label: { Image(systemName: "plus.circle.fill") }) .font(.largeTitle) } ZStack { Circle() .stroke(Color.blue, lineWidth: 2) .padding(70) CircleLayout { ForEach( Array(0 ..< count), id: \.self ) { i in Text("#\(i)") .padding() .background { Color(white: 0.9) } .clipShape(Capsule()) .overlay { Circle() .stroke(Color.blue, lineWidth: 4) } } } .font(.title) } } } } #Preview { SampleCircleLayoutView() }
