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
+85
View File
@@ -332,6 +332,13 @@ struct MealMoodWidgetView: View {
// MARK: - Widget definition
@main
struct MealMoodWidgetBundle: WidgetBundle {
var body: some Widget {
MealMoodWidget()
MealMoodWeekWidget()
}
}
struct MealMoodWidget: Widget {
let kind = "MealMoodWidget"
@@ -344,3 +351,81 @@ struct MealMoodWidget: Widget {
.supportedFamilies([.systemSmall, .systemMedium, .accessoryRectangular, .accessoryInline])
}
}
// MARK: - Week widget (2.1.0)
struct WeekEntry: TimelineEntry {
let date: Date
let payload: WatchWeekPayload?
}
struct WeekProvider: TimelineProvider {
func placeholder(in context: Context) -> WeekEntry {
WeekEntry(date: Date(), payload: WatchWeekPayload.stored())
}
func getSnapshot(in context: Context, completion: @escaping (WeekEntry) -> Void) {
completion(WeekEntry(date: Date(), payload: WatchWeekPayload.stored()))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<WeekEntry>) -> Void) {
let entry = WeekEntry(date: Date(), payload: WatchWeekPayload.stored())
let nextMidnight = Calendar.current.startOfDay(for: Date()).addingTimeInterval(86_400 + 300)
completion(Timeline(entries: [entry], policy: .after(nextMidnight)))
}
}
struct MealMoodWeekWidget: Widget {
let kind = "MealMoodWeekWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: WeekProvider()) { entry in
WeekWidgetView(entry: entry)
.containerBackground(Color(red: 1.0, green: 0.97, blue: 0.94), for: .widget)
}
.configurationDisplayName("MealMood — Semana")
.description("Your whole week at a glance.")
.supportedFamilies([.systemMedium, .systemLarge])
}
}
struct WeekWidgetView: View {
@Environment(\.widgetFamily) private var family
let entry: WeekEntry
var body: some View {
Group {
if let payload = entry.payload, !payload.days.isEmpty {
VStack(alignment: .leading, spacing: family == .systemLarge ? 6 : 3) {
ForEach(payload.days, id: \.dayOfWeek) { day in
HStack(alignment: .firstTextBaseline, spacing: 6) {
Text(day.title)
.font(.system(size: family == .systemLarge ? 13 : 10, weight: .bold, design: .rounded))
.foregroundStyle(day.isToday ? Color(red: 1.0, green: 0.45, blue: 0.35) : .primary)
.frame(width: family == .systemLarge ? 62 : 48, alignment: .leading)
Text(mealsLine(day))
.font(.system(size: family == .systemLarge ? 12 : 10, design: .rounded))
.foregroundStyle(.secondary)
.lineLimit(family == .systemLarge ? 2 : 1)
Spacer(minLength: 0)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
} else {
VStack(spacing: 4) {
Image(systemName: "calendar")
.foregroundStyle(.secondary)
Text(verbatim: "MealMood")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
}
.widgetURL(URL(string: "mealmood://today"))
}
private func mealsLine(_ day: WatchWeekPayload.Day) -> String {
day.meals.compactMap(\.name).joined(separator: " · ")
}
}