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:
@@ -0,0 +1,114 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
/// Watch complication: today's meals from the last snapshot the watch app
|
||||
/// stored in the app group. Refreshed via WidgetCenter whenever the watch app
|
||||
/// receives new data from the iPhone.
|
||||
@main
|
||||
struct MealMoodWatchWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
MealMoodWatchWidget()
|
||||
}
|
||||
}
|
||||
|
||||
struct TodayEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let day: WatchWeekPayload.Day?
|
||||
}
|
||||
|
||||
struct TodayProvider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> TodayEntry {
|
||||
TodayEntry(date: Date(), day: sampleDay)
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (TodayEntry) -> Void) {
|
||||
completion(TodayEntry(date: Date(), day: currentDay ?? sampleDay))
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<TodayEntry>) -> Void) {
|
||||
let entry = TodayEntry(date: Date(), day: currentDay)
|
||||
// Refresh after midnight so "today" rolls over even without new pushes.
|
||||
let nextMidnight = Calendar.current.startOfDay(for: Date()).addingTimeInterval(86_400 + 300)
|
||||
completion(Timeline(entries: [entry], policy: .after(nextMidnight)))
|
||||
}
|
||||
|
||||
private var currentDay: WatchWeekPayload.Day? {
|
||||
guard let payload = WatchWeekPayload.stored() else { return nil }
|
||||
let todayOffset = (Calendar.current.component(.weekday, from: Date()) + 5) % 7
|
||||
return payload.days.first { $0.dayOfWeek == todayOffset } ?? payload.days.first(where: \.isToday)
|
||||
}
|
||||
|
||||
private var sampleDay: WatchWeekPayload.Day {
|
||||
WatchWeekPayload.Day(
|
||||
dayOfWeek: 0, title: "Lun 8", isToday: true,
|
||||
meals: [
|
||||
.init(type: "lunch", label: "Comida", name: "Ensalada César"),
|
||||
.init(type: "dinner", label: "Cena", name: "Salmón al horno")
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct MealMoodWatchWidget: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: "MealMoodWatchToday", provider: TodayProvider()) { entry in
|
||||
TodayComplicationView(entry: entry)
|
||||
.containerBackground(.clear, for: .widget)
|
||||
}
|
||||
.configurationDisplayName("MealMood")
|
||||
.supportedFamilies([.accessoryRectangular, .accessoryInline, .accessoryCircular])
|
||||
}
|
||||
}
|
||||
|
||||
struct TodayComplicationView: View {
|
||||
@Environment(\.widgetFamily) private var family
|
||||
let entry: TodayEntry
|
||||
|
||||
var body: some View {
|
||||
switch family {
|
||||
case .accessoryInline:
|
||||
if let meal = nextMeal {
|
||||
Text("\(meal.label): \(meal.name ?? "—")")
|
||||
} else {
|
||||
Text("MealMood")
|
||||
}
|
||||
case .accessoryCircular:
|
||||
VStack(spacing: 0) {
|
||||
Image(systemName: WatchWeekPayload.icon(for: nextMeal?.type ?? "dinner"))
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
Text(nextMeal?.name?.prefix(6) ?? "—")
|
||||
.font(.system(size: 9))
|
||||
.lineLimit(1)
|
||||
}
|
||||
default:
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
if let day = entry.day {
|
||||
ForEach(Array(day.meals.prefix(3).enumerated()), id: \.offset) { _, meal in
|
||||
HStack(spacing: 3) {
|
||||
Image(systemName: WatchWeekPayload.icon(for: meal.type))
|
||||
.font(.system(size: 9))
|
||||
Text(meal.name ?? "—")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(verbatim: "MealMood")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
Image(systemName: "iphone.and.arrow.forward")
|
||||
.font(.system(size: 10))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
/// Lunch until 16:00 local, dinner after — simple heuristic for the small families.
|
||||
private var nextMeal: WatchWeekPayload.Meal? {
|
||||
guard let meals = entry.day?.meals, !meals.isEmpty else { return nil }
|
||||
let hour = Calendar.current.component(.hour, from: entry.date)
|
||||
if hour >= 16, let dinner = meals.last(where: { $0.name != nil }) { return dinner }
|
||||
return meals.first(where: { $0.name != nil }) ?? meals.first
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user