MacroRing: - Animates from 0 to target on appear (spring, 1.0s response) - Center value fades in + scales up with 0.4s delay - Updates animate smoothly on data change - .contentTransition(.numericText()) on calorie count MacroBar: - Bar width animates from 0 to target on appear (spring, 0.3s delay) - Updates animate smoothly on data change - .contentTransition(.numericText()) on values TodayView: - Meal sections stagger in: fade up with 0.08s delay per card - Re-animates on tab switch (onAppear resets animated flag) - Re-animates on date change - Spring physics for natural feel Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
90 lines
2.6 KiB
Swift
90 lines
2.6 KiB
Swift
import SwiftUI
|
|
|
|
struct MacroRing: View {
|
|
let consumed: Double
|
|
let goal: Double
|
|
var lineWidth: CGFloat = 10
|
|
var color: Color = .emerald
|
|
var size: CGFloat = 100
|
|
|
|
@State private var animatedProgress: Double = 0
|
|
|
|
private var targetProgress: Double {
|
|
guard goal > 0 else { return 0 }
|
|
return min(max(consumed / goal, 0), 1.0)
|
|
}
|
|
|
|
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))
|
|
}
|
|
.frame(width: size, height: size)
|
|
.onAppear {
|
|
animatedProgress = 0
|
|
withAnimation(.spring(response: 1.0, dampingFraction: 0.7).delay(0.2)) {
|
|
animatedProgress = targetProgress
|
|
}
|
|
}
|
|
.onChange(of: consumed) { _, _ in
|
|
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
|
|
animatedProgress = targetProgress
|
|
}
|
|
}
|
|
.onChange(of: goal) { _, _ in
|
|
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
|
|
animatedProgress = targetProgress
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct MacroRingWithLabel: View {
|
|
let consumed: Double
|
|
let goal: Double
|
|
let label: String
|
|
var color: Color = .emerald
|
|
var size: CGFloat = 100
|
|
var lineWidth: CGFloat = 10
|
|
|
|
@State private var showValue = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
MacroRing(
|
|
consumed: consumed,
|
|
goal: goal,
|
|
lineWidth: lineWidth,
|
|
color: color,
|
|
size: size
|
|
)
|
|
|
|
VStack(spacing: 2) {
|
|
Text("\(Int(consumed))")
|
|
.font(.system(size: size * 0.22, weight: .bold, design: .rounded))
|
|
.foregroundStyle(Color.textPrimary)
|
|
.contentTransition(.numericText())
|
|
Text(label)
|
|
.font(.system(size: size * 0.1, weight: .medium))
|
|
.foregroundStyle(Color.textSecondary)
|
|
}
|
|
.opacity(showValue ? 1 : 0)
|
|
.scaleEffect(showValue ? 1 : 0.5)
|
|
}
|
|
.onAppear {
|
|
showValue = false
|
|
withAnimation(.spring(response: 0.6, dampingFraction: 0.7).delay(0.4)) {
|
|
showValue = true
|
|
}
|
|
}
|
|
}
|
|
}
|