Files
FamilyMealPlanner/MealMoodWatch/TodayView.swift
T
alexandrev-tibco 4579b9bb69 watch: enviar la semana aunque todavia no este planificada
Faltaba la otra mitad del "no se actualiza": makePayload devolvia nil cuando la
semana en curso no tenia plan creado, asi que el iPhone no mandaba nada — ni
respondia cuando el reloj pedia datos — y el reloj se quedaba con el ultimo
snapshot, el del miercoles. Ahora la semana viaja igualmente, vacia, y el reloj
dice que no hay nada planificado en vez de enseñar lo de hace dias.

Ademas el reloj muestra cuando se actualizo por ultima vez. Sin eso, un
snapshot viejo y uno recien llegado se ven igual, y no hay forma de saber si
el problema es el envio o lo que se pinta.

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 16:32:34 +02:00

172 lines
6.1 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()
}
// When the watch is holding an old snapshot, this is what makes
// it obvious instead of guessing why the meals look wrong.
if let payload = store.payload {
LastUpdatedFooter(payload: payload)
}
}
}
}
}
private struct LastUpdatedFooter: View {
let payload: WatchWeekPayload
var body: some View {
HStack(spacing: 3) {
Image(systemName: "arrow.clockwise")
// Label comes localized from the iPhone; the date formats itself
// with the watch's own locale.
Text("\(payload.updatedLabel ?? "") \(payload.updatedAt.formatted(date: .abbreviated, time: .shortened))")
}
.font(.system(size: 9, weight: .medium, design: .rounded))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity)
.padding(.top, 6)
}
}
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)
}
}