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
This commit is contained in:
@@ -5,6 +5,7 @@ import WidgetKit
|
||||
@main
|
||||
struct MealMoodWatchApp: App {
|
||||
@StateObject private var store = WatchWeekStore.shared
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
@@ -14,6 +15,11 @@ struct MealMoodWatchApp: App {
|
||||
}
|
||||
.tabViewStyle(.verticalPage)
|
||||
.environmentObject(store)
|
||||
// Opening the app is exactly when a snapshot from another day would
|
||||
// be on screen, so ask the phone for a fresh one.
|
||||
.onChange(of: scenePhase) { _, phase in
|
||||
if phase == .active { store.refresh() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +65,13 @@ final class WatchWeekStore: NSObject, ObservableObject, WCSessionDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/// Asks the phone for an up-to-date snapshot. The phone rebuilds it from its
|
||||
/// store, so this also fixes a snapshot left over from a previous week.
|
||||
func refresh() {
|
||||
guard WCSession.isSupported() else { return }
|
||||
requestFromPhone(WCSession.default)
|
||||
}
|
||||
|
||||
private func requestFromPhone(_ session: WCSession) {
|
||||
guard session.isReachable else { return }
|
||||
session.sendMessage(["request": "week"], replyHandler: { [weak self] reply in
|
||||
@@ -78,7 +91,11 @@ final class WatchWeekStore: NSObject, ObservableObject, WCSessionDelegate {
|
||||
}
|
||||
|
||||
func sessionReachabilityDidChange(_ session: WCSession) {
|
||||
if payload == nil { requestFromPhone(session) }
|
||||
// Refresh when there's nothing yet, and also when what we have no longer
|
||||
// covers today — otherwise the watch keeps showing an old day.
|
||||
if payload == nil || payload?.isStale() == true {
|
||||
requestFromPhone(session)
|
||||
}
|
||||
}
|
||||
|
||||
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
|
||||
|
||||
@@ -5,8 +5,11 @@ import SwiftUI
|
||||
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?.days.first(where: \.isToday) ?? store.payload?.days.first
|
||||
store.payload?.currentDay()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -24,9 +27,18 @@ struct TodayView: View {
|
||||
}
|
||||
.padding(.horizontal, 2)
|
||||
|
||||
ForEach(Array(today.meals.enumerated()), id: \.offset) { _, meal in
|
||||
MealCard(meal: meal)
|
||||
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()
|
||||
}
|
||||
@@ -37,6 +49,9 @@ struct TodayView: View {
|
||||
|
||||
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) }
|
||||
|
||||
@@ -51,9 +66,9 @@ struct MealCard: View {
|
||||
}
|
||||
.foregroundStyle(.white.opacity(0.85))
|
||||
|
||||
Text(meal.name ?? "—")
|
||||
Text(meal.name ?? emptyText ?? "—")
|
||||
.font(.system(.body, design: .rounded, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
.foregroundStyle(.white.opacity(meal.name == nil ? 0.7 : 1))
|
||||
.lineLimit(2)
|
||||
.minimumScaleFactor(0.8)
|
||||
}
|
||||
@@ -71,6 +86,48 @@ struct MealCard: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
|
||||
@@ -16,7 +16,9 @@ struct WeekView: View {
|
||||
.padding(.horizontal, 2)
|
||||
|
||||
ForEach(payload.days, id: \.dayOfWeek) { day in
|
||||
DayRow(day: day)
|
||||
// Nothing is "today" in a snapshot that no longer
|
||||
// covers today.
|
||||
DayRow(day: day, isToday: payload.currentDay()?.dayOfWeek == day.dayOfWeek)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +31,9 @@ struct WeekView: View {
|
||||
|
||||
private struct DayRow: View {
|
||||
let day: WatchWeekPayload.Day
|
||||
/// Resolved by the payload when the view renders, not by the snapshot's own
|
||||
/// frozen `isToday`.
|
||||
let isToday: Bool
|
||||
|
||||
/// "Lun 8" → ("Lun", "8")
|
||||
private var parts: (name: String, number: String) {
|
||||
@@ -42,13 +47,13 @@ private struct DayRow: View {
|
||||
VStack(spacing: 0) {
|
||||
Text(parts.name.uppercased())
|
||||
.font(.system(size: 9, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(day.isToday ? WatchTheme.coral : .secondary)
|
||||
.foregroundStyle(isToday ? WatchTheme.coral : .secondary)
|
||||
Text(parts.number)
|
||||
.font(.system(size: 15, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(day.isToday ? .white : .primary)
|
||||
.foregroundStyle(isToday ? .white : .primary)
|
||||
.frame(width: 26, height: 26)
|
||||
.background(
|
||||
Circle().fill(day.isToday ? WatchTheme.coral : .clear)
|
||||
Circle().fill(isToday ? WatchTheme.coral : .clear)
|
||||
)
|
||||
}
|
||||
.frame(width: 32)
|
||||
@@ -70,7 +75,7 @@ private struct DayRow: View {
|
||||
.padding(.vertical, 5)
|
||||
.padding(.horizontal, 7)
|
||||
.background(
|
||||
day.isToday ? WatchTheme.coral.opacity(0.16) : Color.white.opacity(0.07),
|
||||
isToday ? WatchTheme.coral.opacity(0.16) : Color.white.opacity(0.07),
|
||||
in: RoundedRectangle(cornerRadius: 9)
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user