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
This commit is contained in:
@@ -483,3 +483,4 @@
|
||||
"watch_empty_meal" = "Nicht geplant";
|
||||
"watch_empty_day" = "Für heute ist nichts geplant";
|
||||
"watch_stale" = "Öffne MealMood auf dem iPhone, um diese Woche zu sehen";
|
||||
"watch_updated" = "Aktualisiert";
|
||||
|
||||
@@ -483,3 +483,4 @@
|
||||
"watch_empty_meal" = "Not planned";
|
||||
"watch_empty_day" = "Nothing planned for today";
|
||||
"watch_stale" = "Open MealMood on your iPhone to see this week";
|
||||
"watch_updated" = "Updated";
|
||||
|
||||
@@ -483,3 +483,4 @@
|
||||
"watch_empty_meal" = "Sin planificar";
|
||||
"watch_empty_day" = "Hoy no hay nada planificado";
|
||||
"watch_stale" = "Abre MealMood en el iPhone para ver esta semana";
|
||||
"watch_updated" = "Actualizado";
|
||||
|
||||
@@ -483,3 +483,4 @@
|
||||
"watch_empty_meal" = "Non planifié";
|
||||
"watch_empty_day" = "Rien de prévu aujourd'hui";
|
||||
"watch_stale" = "Ouvrez MealMood sur l'iPhone pour voir cette semaine";
|
||||
"watch_updated" = "Mis à jour";
|
||||
|
||||
@@ -483,3 +483,4 @@
|
||||
"watch_empty_meal" = "Non pianificato";
|
||||
"watch_empty_day" = "Oggi non c'è niente di pianificato";
|
||||
"watch_stale" = "Apri MealMood sull'iPhone per vedere questa settimana";
|
||||
"watch_updated" = "Aggiornato";
|
||||
|
||||
@@ -483,3 +483,4 @@
|
||||
"watch_empty_meal" = "Sem planejamento";
|
||||
"watch_empty_day" = "Nada planejado para hoje";
|
||||
"watch_stale" = "Abra o MealMood no iPhone para ver esta semana";
|
||||
"watch_updated" = "Atualizado";
|
||||
|
||||
@@ -46,7 +46,8 @@ final class WatchSyncService: NSObject, WCSessionDelegate, @unchecked Sendable {
|
||||
let descriptor = FetchDescriptor<WeekPlan>(
|
||||
predicate: #Predicate<WeekPlan> { plan in plan.weekStartDate == weekStart }
|
||||
)
|
||||
guard let plan = try? context.fetch(descriptor).first else { return nil }
|
||||
// No plan for this week yet? Answer with the empty week anyway.
|
||||
let plan = try? context.fetch(descriptor).first
|
||||
let dishes = (try? context.fetch(FetchDescriptor<Dish>())) ?? []
|
||||
|
||||
return Self.makePayload(plan: plan, dishes: dishes, settings: settings)?.encoded()
|
||||
@@ -67,8 +68,13 @@ final class WatchSyncService: NSObject, WCSessionDelegate, @unchecked Sendable {
|
||||
try? WCSession.default.updateApplicationContext([WatchWeekPayload.storageKey: data])
|
||||
}
|
||||
|
||||
/// Builds the snapshot for a week. A missing plan is NOT a reason to stay
|
||||
/// quiet: without this the watch keeps whatever it had — which is how it
|
||||
/// went on showing Wednesday's dinner on Saturday. An empty week travels
|
||||
/// too, and the watch says there's nothing planned.
|
||||
static func makePayload(plan: WeekPlan?, dishes: [Dish], settings: AppSettings) -> WatchWeekPayload? {
|
||||
guard let plan else { return nil }
|
||||
let weekStart = plan?.weekStartDate ?? Date().startOfWeek()
|
||||
let slots = plan?.slotList ?? []
|
||||
let locale = Locale(identifier: settings.languageEnum.resolved().localeIdentifier)
|
||||
let dishById = Dictionary(dishes.map { ($0.id, $0) }, uniquingKeysWith: { first, _ in first })
|
||||
let dayFormatter = DateFormatter()
|
||||
@@ -76,12 +82,12 @@ final class WatchSyncService: NSObject, WCSessionDelegate, @unchecked Sendable {
|
||||
dayFormatter.setLocalizedDateFormatFromTemplate("EEE d")
|
||||
|
||||
let todayOffset = (Calendar.current.component(.weekday, from: Date()) + 5) % 7
|
||||
let isCurrentWeek = plan.weekStartDate == Date().startOfWeek()
|
||||
let isCurrentWeek = weekStart == Date().startOfWeek()
|
||||
let schedule = settings.schedule(for: plan)
|
||||
|
||||
let days: [WatchWeekPayload.Day] = schedule.dayRange.map { day in
|
||||
let meals: [WatchWeekPayload.Meal] = schedule.mealTypes.map { meal in
|
||||
let slot = plan.slotList.first { $0.dayOfWeek == day && $0.mealType == meal.rawValue }
|
||||
let slot = slots.first { $0.dayOfWeek == day && $0.mealType == meal.rawValue }
|
||||
let label = String(localized: String.LocalizationValue(meal.localizedKey))
|
||||
var name: String?
|
||||
if let slot {
|
||||
@@ -98,7 +104,7 @@ final class WatchSyncService: NSObject, WCSessionDelegate, @unchecked Sendable {
|
||||
}
|
||||
return WatchWeekPayload.Meal(type: meal.rawValue, label: label, name: name)
|
||||
}
|
||||
let dayDate = plan.weekStartDate.addingDays(day)
|
||||
let dayDate = weekStart.addingDays(day)
|
||||
return WatchWeekPayload.Day(
|
||||
dayOfWeek: day,
|
||||
title: dayFormatter.string(from: dayDate).capitalized(with: locale),
|
||||
@@ -111,12 +117,13 @@ final class WatchSyncService: NSObject, WCSessionDelegate, @unchecked Sendable {
|
||||
}
|
||||
|
||||
return WatchWeekPayload(
|
||||
weekTitle: plan.weekStartDate.formattedWeekRange(),
|
||||
weekTitle: weekStart.formattedWeekRange(),
|
||||
days: days,
|
||||
updatedAt: Date(),
|
||||
emptyMealText: String(localized: "watch_empty_meal"),
|
||||
emptyDayText: String(localized: "watch_empty_day"),
|
||||
staleText: String(localized: "watch_stale")
|
||||
staleText: String(localized: "watch_stale"),
|
||||
updatedLabel: String(localized: "watch_updated")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,9 @@ struct WatchWeekPayload: Codable {
|
||||
var emptyMealText: String?
|
||||
var emptyDayText: String?
|
||||
var staleText: String?
|
||||
/// "Updated" — shown next to `updatedAt` so it's visible at a glance when
|
||||
/// the watch is holding an old snapshot.
|
||||
var updatedLabel: String?
|
||||
|
||||
static let appGroupID = "group.com.alexandrevazquez.mealmood"
|
||||
static let storageKey = "watch_week_payload_v1"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import XCTest
|
||||
import SwiftData
|
||||
@testable import MealMood
|
||||
|
||||
/// Regression suite for "the watch showed Wednesday's meal on Saturday".
|
||||
@@ -153,6 +154,35 @@ final class WatchWeekPayloadTests: XCTestCase {
|
||||
XCTAssertTrue(payload.isStale(now: saturday, calendar: calendar))
|
||||
}
|
||||
|
||||
|
||||
// MARK: - A week with no plan yet
|
||||
|
||||
@MainActor
|
||||
func testWeekWithoutAPlanStillProducesASnapshot() throws {
|
||||
// Staying quiet here is what left the watch showing the previous
|
||||
// week: no plan meant no push at all, so the old snapshot survived.
|
||||
let container = try ModelContainer(
|
||||
for: AppSettings.self, Dish.self, Tag.self, WeekPlan.self, MealSlot.self, ShoppingItem.self,
|
||||
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
|
||||
)
|
||||
let context = ModelContext(container)
|
||||
let settings = AppSettings()
|
||||
settings.activeMealTypes = [.lunch, .dinner]
|
||||
settings.includeWeekends = true
|
||||
context.insert(settings)
|
||||
|
||||
let payload = try XCTUnwrap(
|
||||
WatchSyncService.makePayload(plan: nil, dishes: [], settings: settings),
|
||||
"a missing plan must still produce the current week"
|
||||
)
|
||||
|
||||
XCTAssertEqual(payload.days.count, 7)
|
||||
XCTAssertNotNil(payload.currentDay(), "today must be in there")
|
||||
XCTAssertTrue(payload.currentDay()?.isEmpty ?? false, "and it must read as nothing planned")
|
||||
XCTAssertFalse(payload.isStale(), "an empty current week is not stale")
|
||||
_ = container
|
||||
}
|
||||
|
||||
func testWeekdayOffsetMapsMondayToZeroAndSundayToSix() {
|
||||
XCTAssertEqual(WatchWeekPayload.weekdayOffset(for: date(2026, 9, 7), calendar: calendar), 0)
|
||||
XCTAssertEqual(WatchWeekPayload.weekdayOffset(for: date(2026, 9, 12), calendar: calendar), 5)
|
||||
|
||||
@@ -42,11 +42,34 @@ struct TodayView: View {
|
||||
} 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
|
||||
|
||||
Reference in New Issue
Block a user