import WidgetKit import SwiftUI // MARK: - Shared data key (main app writes, widget reads) // Uses standard UserDefaults for now. Migrate to App Group // UserDefaults when App Group is configured in Xcode. private let caloriesKey = "widget_totalCalories" private let goalKey = "widget_calorieGoal" // MARK: - Timeline struct CalorieEntry: TimelineEntry { let date: Date let totalCalories: Double let calorieGoal: Double var progress: Double { guard calorieGoal > 0 else { return 0 } return min(max(totalCalories / calorieGoal, 0), 1.0) } var remaining: Double { max(calorieGoal - totalCalories, 0) } } struct CalorieProvider: TimelineProvider { func placeholder(in context: Context) -> CalorieEntry { CalorieEntry(date: .now, totalCalories: 845, calorieGoal: 2000) } func getSnapshot(in context: Context, completion: @escaping (CalorieEntry) -> Void) { completion(readEntry()) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { let entry = readEntry() // Refresh every 15 minutes let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: entry.date)! let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) completion(timeline) } private func readEntry() -> CalorieEntry { let defaults = UserDefaults.standard let calories = defaults.double(forKey: caloriesKey) let goal = defaults.double(forKey: goalKey) return CalorieEntry( date: .now, totalCalories: calories, calorieGoal: goal > 0 ? goal : 2000 ) } } // MARK: - Widget Views struct CalorieRingView: View { let entry: CalorieEntry let size: CGFloat let lineWidth: CGFloat private let ringColor = Color(red: 0.020, green: 0.588, blue: 0.412) // emerald var body: some View { ZStack { // Background ring Circle() .stroke(ringColor.opacity(0.2), lineWidth: lineWidth) // Progress ring Circle() .trim(from: 0, to: entry.progress) .stroke(ringColor, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)) .rotationEffect(.degrees(-90)) // Center text VStack(spacing: 1) { Text("\(Int(entry.totalCalories))") .font(.system(size: size * 0.22, weight: .bold, design: .rounded)) .foregroundStyle(.primary) Text("/ \(Int(entry.calorieGoal))") .font(.system(size: size * 0.1, weight: .medium, design: .rounded)) .foregroundStyle(.secondary) } } .frame(width: size, height: size) } } struct SmallWidgetView: View { let entry: CalorieEntry var body: some View { VStack(spacing: 8) { CalorieRingView(entry: entry, size: 90, lineWidth: 8) Text("\(Int(entry.remaining)) left") .font(.caption2.weight(.medium)) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity, maxHeight: .infinity) } } struct MediumWidgetView: View { let entry: CalorieEntry var body: some View { HStack(spacing: 20) { CalorieRingView(entry: entry, size: 100, lineWidth: 9) VStack(alignment: .leading, spacing: 6) { Text("Calories") .font(.headline) .foregroundStyle(.primary) Text("\(Int(entry.totalCalories)) of \(Int(entry.calorieGoal))") .font(.subheadline) .foregroundStyle(.secondary) Text("\(Int(entry.remaining)) remaining") .font(.caption) .foregroundStyle(.tertiary) } } .frame(maxWidth: .infinity, maxHeight: .infinity) } } // Lock screen widgets struct CircularWidgetView: View { let entry: CalorieEntry var body: some View { Gauge(value: entry.progress) { Text("\(Int(entry.totalCalories))") .font(.system(size: 12, weight: .bold, design: .rounded)) } .gaugeStyle(.accessoryCircular) .tint(Color(red: 0.020, green: 0.588, blue: 0.412)) } } struct InlineWidgetView: View { let entry: CalorieEntry var body: some View { Text("🔥 \(Int(entry.totalCalories)) / \(Int(entry.calorieGoal)) cal") } } struct RectangularWidgetView: View { let entry: CalorieEntry var body: some View { HStack(spacing: 8) { Gauge(value: entry.progress) { EmptyView() } .gaugeStyle(.accessoryLinear) .tint(Color(red: 0.020, green: 0.588, blue: 0.412)) Text("\(Int(entry.totalCalories)) cal") .font(.system(size: 13, weight: .bold, design: .rounded)) } } } // MARK: - Widget Configuration struct PlatformWidget: Widget { let kind: String = "PlatformWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: CalorieProvider()) { entry in PlatformWidgetEntryView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("Calories") .description("Today's calorie progress ring.") .supportedFamilies([ .systemSmall, .systemMedium, .accessoryCircular, .accessoryInline, .accessoryRectangular, ]) } } // Use @ViewBuilder to pick the right view per family struct PlatformWidgetEntryView: View { @Environment(\.widgetFamily) var family let entry: CalorieEntry var body: some View { switch family { case .systemSmall: SmallWidgetView(entry: entry) case .systemMedium: MediumWidgetView(entry: entry) case .accessoryCircular: CircularWidgetView(entry: entry) case .accessoryInline: InlineWidgetView(entry: entry) case .accessoryRectangular: RectangularWidgetView(entry: entry) default: SmallWidgetView(entry: entry) } } } #Preview(as: .systemSmall) { PlatformWidget() } timeline: { CalorieEntry(date: .now, totalCalories: 845, calorieGoal: 2000) }