Files
FamilyMealPlanner/MealMoodWatch/TodayView.swift
T
alexandrev-tibco a049e4f74b watch: el dia de hoy se resuelve al pintar, no al generar el envio
El iPhone marcaba isToday al construir el snapshot y el reloj se creia esa
marca para siempre. Como el snapshot sobrevive en el app group, un sabado
seguia enseñando la cena del miercoles: el dia que era cuando se genero.

Ahora cada dia viaja con su fecha real y el reloj busca hoy cuando dibuja. Si
el snapshot es de otra semana no hay "hoy" que enseñar, asi que en vez de
colar la comida de otro dia lo dice y pide abrir el iPhone. Para snapshots
guardados por versiones anteriores, que no traen fecha, solo se confia en el
indice del dia mientras el propio snapshot sea de esta semana.

Ademas el reloj pide datos al abrirse y el iPhone, en vez de responder con lo
ultimo que cacheo, reconstruye la semana desde el store — que es la otra mitad
del "no se actualiza": el cache podia ser de hace dias.

Y cuando no hay nada planificado se dice, que antes era un guion indistinguible
de una comida marcada como no planificada: "Hoy no hay nada planificado" para
el dia entero y "Sin planificar" por comida, en los 6 idiomas (el bundle del
reloj no tiene cadenas propias, viajan en el envio).

Refs #34, #35

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013su1ttRiMeMYxkZJ1Y3246
2026-09-12 13:21:21 +02:00

149 lines
5.2 KiB
Swift

import SwiftUI
/// Today page Weather-app style: a bold colored header with the day, then
/// one vivid gradient card per meal.
struct TodayView: View {
@EnvironmentObject private var store: WatchWeekStore
/// Resolved as the view renders. The snapshot's own `isToday` is frozen at
/// the moment the iPhone built it, which is how a Wednesday plan ended up
/// showing on a Saturday.
private var today: WatchWeekPayload.Day? {
store.payload?.currentDay()
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 8) {
if let today {
HStack(alignment: .firstTextBaseline) {
Text(today.title)
.font(.system(.title3, design: .rounded, weight: .bold))
.foregroundStyle(WatchTheme.coral)
Spacer()
Image(systemName: "fork.knife")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.horizontal, 2)
if today.isEmpty {
EmptyDayView(text: store.payload?.emptyDayText)
} else {
ForEach(Array(today.meals.enumerated()), id: \.offset) { _, meal in
MealCard(meal: meal, emptyText: store.payload?.emptyMealText)
}
}
} else if let payload = store.payload {
// There is a snapshot, but it doesn't cover today: better to
// say so than to show another day's meals as if they were
// today's.
StaleView(text: payload.staleText)
} else {
EmptySyncView()
}
}
}
}
}
struct MealCard: View {
let meal: WatchWeekPayload.Meal
/// Localized "nothing planned" coming from the iPhone the watch bundle
/// has no strings of its own.
var emptyText: String?
private var accent: Color { WatchTheme.accent(for: meal.type) }
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 5) {
Image(systemName: WatchWeekPayload.icon(for: meal.type))
.font(.system(size: 11, weight: .bold))
Text(meal.label.uppercased())
.font(.system(size: 11, weight: .bold, design: .rounded))
.tracking(0.5)
}
.foregroundStyle(.white.opacity(0.85))
Text(meal.name ?? emptyText ?? "")
.font(.system(.body, design: .rounded, weight: .semibold))
.foregroundStyle(.white.opacity(meal.name == nil ? 0.7 : 1))
.lineLimit(2)
.minimumScaleFactor(0.8)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 10)
.padding(.vertical, 9)
.background(
LinearGradient(
colors: [accent, accent.opacity(0.65)],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
in: RoundedRectangle(cornerRadius: 12)
)
}
}
/// The day exists in the snapshot but has no meals planned.
struct EmptyDayView: View {
var text: String?
var body: some View {
VStack(spacing: 6) {
Image(systemName: "moon.zzz.fill")
.font(.title3)
.foregroundStyle(WatchTheme.coral)
if let text {
Text(text)
.font(.system(.caption2, design: .rounded, weight: .semibold))
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
}
}
.frame(maxWidth: .infinity)
.padding(.top, 18)
}
}
/// A snapshot from another week: the watch has data, but not for today.
struct StaleView: View {
var text: String?
var body: some View {
VStack(spacing: 6) {
Image(systemName: "iphone.gen3.radiowaves.left.and.right")
.font(.title3)
.foregroundStyle(WatchTheme.coral)
if let text {
Text(text)
.font(.system(.caption2, design: .rounded, weight: .semibold))
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
}
}
.frame(maxWidth: .infinity)
.padding(.top, 18)
}
}
struct EmptySyncView: View {
var body: some View {
VStack(spacing: 8) {
Image(systemName: "applewatch.radiowaves.left.and.right")
.font(.title3)
.foregroundStyle(WatchTheme.coral)
// Payload arrives fully localized from the iPhone; the watch
// bundle carries no strings of its own, so this stays neutral.
Text(verbatim: "MealMood")
.font(.system(.caption2, design: .rounded, weight: .semibold))
Image(systemName: "iphone.gen3")
.font(.caption2)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.top, 24)
}
}