diff --git a/MealMoodWatchWidget/Info.plist b/MealMoodWatchWidget/Info.plist
index cdf3c3e..8d20719 100644
--- a/MealMoodWatchWidget/Info.plist
+++ b/MealMoodWatchWidget/Info.plist
@@ -17,7 +17,7 @@
CFBundleShortVersionString
2.1.0
CFBundleVersion
- 81
+ 82
NSExtension
NSExtensionPointIdentifier
diff --git a/MealMoodWatchWidget/MealMoodWatchWidget.swift b/MealMoodWatchWidget/MealMoodWatchWidget.swift
index 6e27d86..4fd58b8 100644
--- a/MealMoodWatchWidget/MealMoodWatchWidget.swift
+++ b/MealMoodWatchWidget/MealMoodWatchWidget.swift
@@ -8,6 +8,7 @@ import SwiftUI
struct MealMoodWatchWidgetBundle: WidgetBundle {
var body: some Widget {
MealMoodWatchWidget()
+ MealMoodWatchDaysWidget()
}
}
@@ -83,16 +84,21 @@ struct TodayComplicationView: View {
default:
VStack(alignment: .leading, spacing: 2) {
if let day = entry.day {
- ForEach(Array(day.meals.prefix(3).enumerated()), id: \.offset) { _, meal in
+ // The rectangular slot fits ~3 lines: with fewer meals the
+ // text scales up so the space isn't wasted.
+ let meals = Array(day.meals.prefix(3))
+ let nameSize: CGFloat = meals.count <= 1 ? 17 : (meals.count == 2 ? 15 : 12)
+ ForEach(Array(meals.enumerated()), id: \.offset) { _, meal in
HStack(spacing: 4) {
Image(systemName: WatchWeekPayload.icon(for: meal.type))
- .font(.system(size: 9, weight: .bold))
+ .font(.system(size: nameSize - 3, weight: .bold))
.foregroundStyle(accent(for: meal.type))
.widgetAccentable()
- .frame(width: 11)
+ .frame(width: nameSize)
Text(meal.name ?? "—")
- .font(.system(size: 12, weight: .semibold, design: .rounded))
- .lineLimit(1)
+ .font(.system(size: nameSize, weight: .semibold, design: .rounded))
+ .lineLimit(meals.count <= 1 ? 3 : (meals.count == 2 ? 2 : 1))
+ .minimumScaleFactor(0.75)
}
}
} else {
@@ -108,6 +114,10 @@ struct TodayComplicationView: View {
}
private func accent(for mealType: String) -> Color {
+ Self.accentColor(for: mealType)
+ }
+
+ static func accentColor(for mealType: String) -> Color {
switch mealType {
case "breakfast": return Color(red: 0.95, green: 0.68, blue: 0.25)
case "lunch": return Color(red: 0.98, green: 0.55, blue: 0.30)
@@ -125,3 +135,102 @@ struct TodayComplicationView: View {
return meals.first(where: { $0.name != nil }) ?? meals.first
}
}
+
+// MARK: - Next days complication
+
+struct DaysEntry: TimelineEntry {
+ let date: Date
+ let days: [WatchWeekPayload.Day]
+}
+
+struct DaysProvider: TimelineProvider {
+ func placeholder(in context: Context) -> DaysEntry {
+ DaysEntry(date: Date(), days: sampleDays)
+ }
+
+ func getSnapshot(in context: Context, completion: @escaping (DaysEntry) -> Void) {
+ let days = upcomingDays
+ completion(DaysEntry(date: Date(), days: days.isEmpty ? sampleDays : days))
+ }
+
+ func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
+ let entry = DaysEntry(date: Date(), days: upcomingDays)
+ let nextMidnight = Calendar.current.startOfDay(for: Date()).addingTimeInterval(86_400 + 300)
+ completion(Timeline(entries: [entry], policy: .after(nextMidnight)))
+ }
+
+ /// Today and the next two planned days (falls back to the week's first
+ /// days when today is outside the planned range, e.g. weekends off).
+ private var upcomingDays: [WatchWeekPayload.Day] {
+ guard let payload = WatchWeekPayload.stored() else { return [] }
+ let todayOffset = (Calendar.current.component(.weekday, from: Date()) + 5) % 7
+ let fromToday = payload.days.filter { $0.dayOfWeek >= todayOffset }
+ let source = fromToday.isEmpty ? payload.days : fromToday
+ return Array(source.prefix(3))
+ }
+
+ private var sampleDays: [WatchWeekPayload.Day] {
+ [
+ .init(dayOfWeek: 0, title: "Lun 8", isToday: true, meals: [
+ .init(type: "lunch", label: "Comida", name: "Ensalada"),
+ .init(type: "dinner", label: "Cena", name: "Salmón")]),
+ .init(dayOfWeek: 1, title: "Mar 9", isToday: false, meals: [
+ .init(type: "dinner", label: "Cena", name: "Lentejas")]),
+ .init(dayOfWeek: 2, title: "Mié 10", isToday: false, meals: [
+ .init(type: "dinner", label: "Cena", name: "Pizza casera")])
+ ]
+ }
+}
+
+struct MealMoodWatchDaysWidget: Widget {
+ var body: some WidgetConfiguration {
+ StaticConfiguration(kind: "MealMoodWatchDays", provider: DaysProvider()) { entry in
+ DaysComplicationView(entry: entry)
+ .containerBackground(.clear, for: .widget)
+ }
+ .configurationDisplayName("MealMood — 3 días")
+ .supportedFamilies([.accessoryRectangular])
+ }
+}
+
+struct DaysComplicationView: View {
+ let entry: DaysEntry
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 1) {
+ if entry.days.isEmpty {
+ Text(verbatim: "MealMood")
+ .font(.system(size: 12, weight: .semibold, design: .rounded))
+ Image(systemName: "iphone.and.arrow.forward")
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ } else {
+ ForEach(entry.days, id: \.dayOfWeek) { day in
+ HStack(alignment: .firstTextBaseline, spacing: 4) {
+ Text(dayAbbrev(day))
+ .font(.system(size: 11, weight: .heavy, design: .rounded))
+ .foregroundStyle(day.isToday
+ ? Color(red: 1.0, green: 0.45, blue: 0.35)
+ : .secondary)
+ .widgetAccentable()
+ .frame(width: 28, alignment: .leading)
+ Text(mealsLine(day))
+ .font(.system(size: 12, weight: .medium, design: .rounded))
+ .lineLimit(1)
+ .minimumScaleFactor(0.7)
+ }
+ }
+ }
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ private func dayAbbrev(_ day: WatchWeekPayload.Day) -> String {
+ String(day.title.split(separator: " ").first ?? "").uppercased()
+ }
+
+ private func mealsLine(_ day: WatchWeekPayload.Day) -> String {
+ let names = day.meals.compactMap(\.name)
+ return names.isEmpty ? "—" : names.joined(separator: " · ")
+ }
+}
diff --git a/MealMoodWidget/Info.plist b/MealMoodWidget/Info.plist
index cdf3c3e..8d20719 100644
--- a/MealMoodWidget/Info.plist
+++ b/MealMoodWidget/Info.plist
@@ -17,7 +17,7 @@
CFBundleShortVersionString
2.1.0
CFBundleVersion
- 81
+ 82
NSExtension
NSExtensionPointIdentifier