65 lines
1.7 KiB
Swift
65 lines
1.7 KiB
Swift
//
|
|
// RadialSegmentedRingView.swift
|
|
// medmap
|
|
//
|
|
// Created by RSA on 15/09/2026.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
/// A single colored arc segment of a `RadialSegmentedRingView`.
|
|
struct RadialRingSegment: Identifiable {
|
|
let id = UUID()
|
|
let percentage: Double
|
|
let color: Color
|
|
}
|
|
|
|
/// Renders a ring made of proportional colored arc segments, with a small gap between each segment.
|
|
struct RadialSegmentedRingView: View {
|
|
|
|
let segments: [RadialRingSegment]
|
|
var lineWidth: CGFloat = 6
|
|
var gapDegrees: Double = 6
|
|
|
|
private var total: Double {
|
|
max(segments.reduce(0) { $0 + $1.percentage }, 0.0001)
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
ForEach(Array(segments.enumerated()), id: \.element.id) { index, segment in
|
|
let (start, end) = arcRange(for: index)
|
|
Circle()
|
|
.trim(from: start, to: end)
|
|
.stroke(segment.color, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
|
|
.rotationEffect(.degrees(-90))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Computes the normalized (0...1) start/end trim values for the segment at `index`, leaving a small gap between segments.
|
|
private func arcRange(for index: Int) -> (CGFloat, CGFloat) {
|
|
let gapFraction = gapDegrees / 360
|
|
var start = 0.0
|
|
for i in 0..<index {
|
|
start += segments[i].percentage / total
|
|
}
|
|
let length = segments[index].percentage / total
|
|
let clampedGap = min(gapFraction, length / 2)
|
|
return (CGFloat(start + clampedGap), CGFloat(start + length - clampedGap))
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
RadialSegmentedRingView(
|
|
segments: [
|
|
RadialRingSegment(percentage: 60, color: .green),
|
|
RadialRingSegment(percentage: 25, color: .orange),
|
|
RadialRingSegment(percentage: 15, color: .red)
|
|
],
|
|
lineWidth: 6
|
|
)
|
|
.frame(width: 92, height: 92)
|
|
.padding(40)
|
|
}
|