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()
}