2.1.0: app de Apple Watch, complicacion y widget semanal en iOS

- Target MealMoodWatch (watchOS 10, SwiftUI): vista de hoy y de la
  semana en TabView vertical; recibe el snapshot del iPhone por
  WatchConnectivity (updateApplicationContext) y lo guarda en su app
  group para la complicacion
- Target MealMoodWatchWidget: complicacion accessoryRectangular/
  Circular/Inline con las comidas de hoy (heuristica comida/cena por
  hora); icono del watch generado desde el logo
- WatchWeekPayload compartido entre iOS app, widget iOS, watch app y
  complicacion — llega ya localizado desde el iPhone
- Widget iOS nuevo "Semana" (systemMedium/Large) con los 7 dias;
  el widget de hoy pasa a WidgetBundle
- Targets creados via gema xcodeproj (sin abrir Xcode); embed del watch
  antes del script de Crashlytics para evitar ciclo de build

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
This commit is contained in:
alexandrev-tibco
2026-09-08 16:38:58 +02:00
parent a9a9d6bdf0
commit c0b485387d
18 changed files with 892 additions and 7 deletions
+82
View File
@@ -0,0 +1,82 @@
import Foundation
import WatchConnectivity
import SwiftData
/// iPhone Watch push of the week snapshot. Uses `updateApplicationContext`,
/// which delivers the latest value even if the watch is unreachable right now.
final class WatchSyncService: NSObject, WCSessionDelegate, @unchecked Sendable {
// WCSession requires a stable delegate; state is confined to WCSession's
// internal queue, hence the @unchecked Sendable.
static let shared = WatchSyncService()
private override init() {
super.init()
guard WCSession.isSupported() else { return }
WCSession.default.delegate = self
WCSession.default.activate()
}
func activate() { /* init side effect */ }
/// Builds and pushes the current + visible week. Call wherever the iOS
/// widget is refreshed so both stay in sync.
func push(plan: WeekPlan?, dishes: [Dish], settings: AppSettings) {
guard WCSession.isSupported(), WCSession.default.activationState == .activated else { return }
guard let payload = Self.makePayload(plan: plan, dishes: dishes, settings: settings),
let data = payload.encoded() else { return }
try? WCSession.default.updateApplicationContext([WatchWeekPayload.storageKey: data])
}
static func makePayload(plan: WeekPlan?, dishes: [Dish], settings: AppSettings) -> WatchWeekPayload? {
guard let plan else { return nil }
let locale = Locale(identifier: settings.languageEnum.resolved().localeIdentifier)
let dishById = Dictionary(dishes.map { ($0.id, $0) }, uniquingKeysWith: { first, _ in first })
let dayFormatter = DateFormatter()
dayFormatter.locale = locale
dayFormatter.setLocalizedDateFormatFromTemplate("EEE d")
let todayOffset = (Calendar.current.component(.weekday, from: Date()) + 5) % 7
let isCurrentWeek = plan.weekStartDate == Date().startOfWeek()
let range = settings.includeWeekends ? 0...6 : 0...4
let days: [WatchWeekPayload.Day] = range.map { day in
let meals: [WatchWeekPayload.Meal] = settings.activeMealTypes.map { meal in
let slot = plan.slotList.first { $0.dayOfWeek == day && $0.mealType == meal.rawValue }
let label = String(localized: String.LocalizationValue(meal.localizedKey))
var name: String?
if let slot {
if slot.isEatingOut {
name = String(localized: "slot_eating_out")
} else if slot.isSkipped {
name = ""
} else if let dishId = slot.dishId, let dish = dishById[dishId] {
name = dish.name
if let secondaryId = slot.secondaryDishId, let secondary = dishById[secondaryId] {
name! += " + \(secondary.name)"
}
}
}
return WatchWeekPayload.Meal(type: meal.rawValue, label: label, name: name)
}
let dayDate = plan.weekStartDate.addingDays(day)
return WatchWeekPayload.Day(
dayOfWeek: day,
title: dayFormatter.string(from: dayDate).capitalized(with: locale),
isToday: isCurrentWeek && day == todayOffset,
meals: meals
)
}
return WatchWeekPayload(
weekTitle: plan.weekStartDate.formattedWeekRange(),
days: days,
updatedAt: Date()
)
}
// MARK: - WCSessionDelegate
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {}
func sessionDidBecomeInactive(_ session: WCSession) {}
func sessionDidDeactivate(_ session: WCSession) { session.activate() }
}