medmap-ios/medmap/Views/Components/RadialSegmentedRingView.swift
2026-09-15 23:23:01 +01:00

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