Files
FamilyMealPlanner/MealMood/Services/WatchWeekPayload.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

116 lines
4.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// Snapshot of the week the iPhone pushes to the Watch (WatchConnectivity) and
/// the watch widget reads from the watch-side app group. Labels and day titles
/// arrive already localized the watch never formats, only renders.
/// Member of the iOS app, watch app and watch widget targets.
struct WatchWeekPayload: Codable {
struct Meal: Codable {
let type: String // MealType rawValue, for the icon
let label: String // localized ("Comida", "Cena")
let name: String? // dish (+ secondary), "Comer fuera", or nil if empty
}
struct Day: Codable {
let dayOfWeek: Int // 0=Monday 6=Sunday
let title: String // localized short title ("Lun 8")
/// `isToday` as of the moment the iPhone built the snapshot. Kept for
/// payloads written by older versions; `date` is what the watch should
/// use, because a stored snapshot outlives the day it was made in.
let isToday: Bool
/// The actual calendar date of this day. Optional: snapshots stored by
/// versions before 2.1.1 don't carry it.
var date: Date?
let meals: [Meal]
init(dayOfWeek: Int, title: String, isToday: Bool, date: Date? = nil, meals: [Meal]) {
self.dayOfWeek = dayOfWeek
self.title = title
self.isToday = isToday
self.date = date
self.meals = meals
}
/// Whether this day is today *now*, asked at render time.
func isCurrentDay(now: Date = Date(), calendar: Calendar = .current) -> Bool {
if let date {
return calendar.isDate(date, inSameDayAs: now)
}
// Pre-2.1.1 snapshot: fall back to the weekday offset, which is only
// right while the snapshot belongs to the current week.
return dayOfWeek == WatchWeekPayload.weekdayOffset(for: now, calendar: calendar)
}
/// Nothing planned at all for this day.
var isEmpty: Bool {
meals.allSatisfy { $0.name == nil }
}
}
let weekTitle: String // localized week range ("8 14 sep")
let days: [Day]
let updatedAt: Date
/// Localized texts the watch cannot build on its own its bundle carries no
/// strings. Optional so older snapshots still decode.
var emptyMealText: String?
var emptyDayText: String?
var staleText: String?
static let appGroupID = "group.com.alexandrevazquez.mealmood"
static let storageKey = "watch_week_payload_v1"
func encoded() -> Data? { try? JSONEncoder().encode(self) }
static func decode(_ data: Data) -> WatchWeekPayload? {
try? JSONDecoder().decode(WatchWeekPayload.self, from: data)
}
/// Reads the last snapshot stored on this device's app group.
static func stored() -> WatchWeekPayload? {
guard let data = UserDefaults(suiteName: appGroupID)?.data(forKey: storageKey) else { return nil }
return decode(data)
}
func store() {
UserDefaults(suiteName: Self.appGroupID)?.set(encoded(), forKey: Self.storageKey)
}
/// 0=Monday 6=Sunday, matching `MealSlot.dayOfWeek`.
static func weekdayOffset(for date: Date, calendar: Calendar = .current) -> Int {
(calendar.component(.weekday, from: date) + 5) % 7
}
/// The day to show as "today", resolved when the view renders rather than
/// when the iPhone built the snapshot. Returns nil when the snapshot is from
/// another week and simply doesn't contain today.
func currentDay(now: Date = Date(), calendar: Calendar = .current) -> Day? {
if days.contains(where: { $0.date != nil }) {
return days.first { $0.isCurrentDay(now: now, calendar: calendar) }
}
// Snapshot written before 2.1.1: no dates, so the weekday offset is the
// only handle and it's trustworthy only while the snapshot itself
// belongs to the current week. Otherwise last week's Saturday would
// pass for today.
guard calendar.isDate(updatedAt, equalTo: now, toGranularity: .weekOfYear) else { return nil }
let offset = Self.weekdayOffset(for: now, calendar: calendar)
return days.first { $0.dayOfWeek == offset }
}
/// True when the snapshot no longer covers today, so showing any of its days
/// would be showing the wrong meal.
func isStale(now: Date = Date(), calendar: Calendar = .current) -> Bool {
currentDay(now: now, calendar: calendar) == nil
}
static func icon(for mealType: String) -> String {
switch mealType {
case "breakfast": return "cup.and.saucer.fill"
case "lunch": return "sun.max.fill"
case "snack": return "carrot.fill"
case "dinner": return "moon.stars.fill"
default: return "fork.knife"
}
}
}