admaDIC App Development & IT Solutions

SwiftUI Custom Circular Layout

by Annett Schwarze | 2025-05-16

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

Custom Circular Layout

 

www.admadic.de | webmaster@admadic.de | Legal Notice and Trademarks | Privacy
© 2005-2007 - admaDIC | All Rights Reserved
All other trademarks and/or registered trademarks are the property of their respective owners
Last Change: Fri May 16 07:42:20 2025 GMT