import SwiftUI struct HealthRing: View { let progress: Double // 0.0 – 1.0 let color: Color var lineWidth: CGFloat = 12 var size: CGFloat = 120 var label: String? = nil @State private var animatedProgress: Double = 0 private var clampedProgress: Double { min(max(progress, 0), 1) } private var percentText: String { "\(Int((animatedProgress * 100).rounded()))" } var body: some View { ZStack { Circle() .stroke(color.opacity(0.15), lineWidth: lineWidth) Circle() .trim(from: 0, to: animatedProgress) .stroke( color, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round) ) .rotationEffect(.degrees(-90)) .animation( .spring(response: 0.85, dampingFraction: 0.72), value: animatedProgress ) VStack(spacing: 2) { HStack(alignment: .firstTextBaseline, spacing: 1) { Text(percentText) .font(.system(size: size * 0.22, weight: .bold, design: .rounded)) .foregroundStyle(.primary) .contentTransition(.numericText(countsDown: animatedProgress < clampedProgress)) .animation( .spring(response: 0.85, dampingFraction: 0.72), value: animatedProgress ) Text("%") .font(.system(size: size * 0.13, weight: .semibold, design: .rounded)) .foregroundStyle(.secondary) } if let label { Text(label) .font(.system(size: size * 0.1, weight: .medium)) .foregroundStyle(.secondary) .lineLimit(1) .minimumScaleFactor(0.5) .frame(maxWidth: size * 0.55) } } } .frame(width: size, height: size) .onAppear { animatedProgress = clampedProgress } .onChange(of: progress) { _, newValue in animatedProgress = min(max(newValue, 0), 1) } } } #Preview { @Previewable @State var cpu: Double = 0.72 @Previewable @State var mem: Double = 0.45 @Previewable @State var disk: Double = 0.88 NavigationStack { ScrollView { LazyVStack(spacing: 32) { HStack(spacing: 24) { VStack(spacing: 8) { HealthRing( progress: cpu, color: Color(.systemBlue), label: "CPU" ) Text("CPU") .font(.caption.weight(.medium)) .foregroundStyle(.secondary) } VStack(spacing: 8) { HealthRing( progress: mem, color: Color(.systemGreen), label: "RAM" ) Text("Memory") .font(.caption.weight(.medium)) .foregroundStyle(.secondary) } VStack(spacing: 8) { HealthRing( progress: disk, color: Color(.systemOrange), label: "Disk" ) Text("Disk") .font(.caption.weight(.medium)) .foregroundStyle(.secondary) } } HealthRing( progress: 0.23, color: Color(.systemRed), lineWidth: 18, size: 180, label: "Error rate" ) VStack(spacing: 8) { Slider(value: $cpu, in: 0...1) Text("CPU: \(Int(cpu * 100))%") .font(.caption) .foregroundStyle(.secondary) } .padding(.horizontal) } .padding() } .navigationTitle("Health") .background(Color(.systemBackground)) } }