bd8090c845
- El primer push se descartaba: updateApplicationContext se llamaba antes de que WCSession terminara de activarse (activacion asincrona). Ahora la sesion se activa al arrancar la app (ContentView), el payload pendiente se envia al completarse la activacion, y ademas el reloj PIDE los datos al abrirse (sendMessage despierta al iPhone en segundo plano y responde con el snapshot del app group) - Rediseno: Hoy con cabecera del dia en coral y tarjetas degradadas por comida (estilo Weather); Semana con circulo de dia a la izquierda y hoy relleno en coral (estilo Calendar); complicacion con iconos tintados por comida y widgetAccentable - Verificado con capturas en simulador de Watch sembrando el app group Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
128 lines
5.1 KiB
Swift
128 lines
5.1 KiB
Swift
import WidgetKit
|
|
import SwiftUI
|
|
|
|
/// Watch complication: today's meals from the last snapshot the watch app
|
|
/// stored in the app group. Refreshed via WidgetCenter whenever the watch app
|
|
/// receives new data from the iPhone.
|
|
@main
|
|
struct MealMoodWatchWidgetBundle: WidgetBundle {
|
|
var body: some Widget {
|
|
MealMoodWatchWidget()
|
|
}
|
|
}
|
|
|
|
struct TodayEntry: TimelineEntry {
|
|
let date: Date
|
|
let day: WatchWeekPayload.Day?
|
|
}
|
|
|
|
struct TodayProvider: TimelineProvider {
|
|
func placeholder(in context: Context) -> TodayEntry {
|
|
TodayEntry(date: Date(), day: sampleDay)
|
|
}
|
|
|
|
func getSnapshot(in context: Context, completion: @escaping (TodayEntry) -> Void) {
|
|
completion(TodayEntry(date: Date(), day: currentDay ?? sampleDay))
|
|
}
|
|
|
|
func getTimeline(in context: Context, completion: @escaping (Timeline<TodayEntry>) -> Void) {
|
|
let entry = TodayEntry(date: Date(), day: currentDay)
|
|
// Refresh after midnight so "today" rolls over even without new pushes.
|
|
let nextMidnight = Calendar.current.startOfDay(for: Date()).addingTimeInterval(86_400 + 300)
|
|
completion(Timeline(entries: [entry], policy: .after(nextMidnight)))
|
|
}
|
|
|
|
private var currentDay: WatchWeekPayload.Day? {
|
|
guard let payload = WatchWeekPayload.stored() else { return nil }
|
|
let todayOffset = (Calendar.current.component(.weekday, from: Date()) + 5) % 7
|
|
return payload.days.first { $0.dayOfWeek == todayOffset } ?? payload.days.first(where: \.isToday)
|
|
}
|
|
|
|
private var sampleDay: WatchWeekPayload.Day {
|
|
WatchWeekPayload.Day(
|
|
dayOfWeek: 0, title: "Lun 8", isToday: true,
|
|
meals: [
|
|
.init(type: "lunch", label: "Comida", name: "Ensalada César"),
|
|
.init(type: "dinner", label: "Cena", name: "Salmón al horno")
|
|
]
|
|
)
|
|
}
|
|
}
|
|
|
|
struct MealMoodWatchWidget: Widget {
|
|
var body: some WidgetConfiguration {
|
|
StaticConfiguration(kind: "MealMoodWatchToday", provider: TodayProvider()) { entry in
|
|
TodayComplicationView(entry: entry)
|
|
.containerBackground(.clear, for: .widget)
|
|
}
|
|
.configurationDisplayName("MealMood")
|
|
.supportedFamilies([.accessoryRectangular, .accessoryInline, .accessoryCircular])
|
|
}
|
|
}
|
|
|
|
struct TodayComplicationView: View {
|
|
@Environment(\.widgetFamily) private var family
|
|
let entry: TodayEntry
|
|
|
|
var body: some View {
|
|
switch family {
|
|
case .accessoryInline:
|
|
if let meal = nextMeal {
|
|
Text("\(meal.label): \(meal.name ?? "—")")
|
|
} else {
|
|
Text("MealMood")
|
|
}
|
|
case .accessoryCircular:
|
|
VStack(spacing: 0) {
|
|
Image(systemName: WatchWeekPayload.icon(for: nextMeal?.type ?? "dinner"))
|
|
.font(.system(size: 14, weight: .semibold))
|
|
Text(nextMeal?.name?.prefix(6) ?? "—")
|
|
.font(.system(size: 9))
|
|
.lineLimit(1)
|
|
}
|
|
default:
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
if let day = entry.day {
|
|
ForEach(Array(day.meals.prefix(3).enumerated()), id: \.offset) { _, meal in
|
|
HStack(spacing: 4) {
|
|
Image(systemName: WatchWeekPayload.icon(for: meal.type))
|
|
.font(.system(size: 9, weight: .bold))
|
|
.foregroundStyle(accent(for: meal.type))
|
|
.widgetAccentable()
|
|
.frame(width: 11)
|
|
Text(meal.name ?? "—")
|
|
.font(.system(size: 12, weight: .semibold, design: .rounded))
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
} else {
|
|
Text(verbatim: "MealMood")
|
|
.font(.system(size: 12, weight: .semibold, design: .rounded))
|
|
Image(systemName: "iphone.and.arrow.forward")
|
|
.font(.system(size: 10))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
}
|
|
|
|
private func accent(for mealType: String) -> Color {
|
|
switch mealType {
|
|
case "breakfast": return Color(red: 0.95, green: 0.68, blue: 0.25)
|
|
case "lunch": return Color(red: 0.98, green: 0.55, blue: 0.30)
|
|
case "snack": return Color(red: 0.62, green: 0.50, blue: 0.83)
|
|
case "dinner": return Color(red: 0.28, green: 0.62, blue: 0.55)
|
|
default: return Color(red: 1.0, green: 0.45, blue: 0.35)
|
|
}
|
|
}
|
|
|
|
/// Lunch until 16:00 local, dinner after — simple heuristic for the small families.
|
|
private var nextMeal: WatchWeekPayload.Meal? {
|
|
guard let meals = entry.day?.meals, !meals.isEmpty else { return nil }
|
|
let hour = Calendar.current.component(.hour, from: entry.date)
|
|
if hour >= 16, let dinner = meals.last(where: { $0.name != nil }) { return dinner }
|
|
return meals.first(where: { $0.name != nil }) ?? meals.first
|
|
}
|
|
}
|