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
@@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "icon-1024.png",
"idiom" : "universal",
"platform" : "watchos",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 681 KiB

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.alexandrevazquez.mealmood</string>
</array>
</dict>
</plist>
+57
View File
@@ -0,0 +1,57 @@
import SwiftUI
import WatchConnectivity
import WidgetKit
@main
struct MealMoodWatchApp: App {
@StateObject private var store = WatchWeekStore.shared
var body: some Scene {
WindowGroup {
TabView {
TodayView()
WeekView()
}
.tabViewStyle(.verticalPage)
.environmentObject(store)
}
}
}
/// Receives the week snapshot from the iPhone and persists it in the watch's
/// app group so the complication can read it too.
final class WatchWeekStore: NSObject, ObservableObject, WCSessionDelegate {
static let shared = WatchWeekStore()
@Published var payload: WatchWeekPayload?
private override init() {
super.init()
payload = WatchWeekPayload.stored()
guard WCSession.isSupported() else { return }
WCSession.default.delegate = self
WCSession.default.activate()
}
private func apply(_ data: Data) {
guard let received = WatchWeekPayload.decode(data) else { return }
DispatchQueue.main.async {
self.payload = received
received.store()
WidgetCenter.shared.reloadAllTimelines()
}
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
// Pick up a context delivered while the app wasn't running.
if let data = session.receivedApplicationContext[WatchWeekPayload.storageKey] as? Data {
apply(data)
}
}
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
if let data = applicationContext[WatchWeekPayload.storageKey] as? Data {
apply(data)
}
}
}
+54
View File
@@ -0,0 +1,54 @@
import SwiftUI
struct TodayView: View {
@EnvironmentObject private var store: WatchWeekStore
private var today: WatchWeekPayload.Day? {
store.payload?.days.first(where: \.isToday)
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 8) {
if let today {
Text(today.title)
.font(.headline)
.foregroundStyle(Color(red: 1.0, green: 0.45, blue: 0.35))
ForEach(Array(today.meals.enumerated()), id: \.offset) { _, meal in
VStack(alignment: .leading, spacing: 2) {
Label(meal.label, systemImage: WatchWeekPayload.icon(for: meal.type))
.font(.caption2)
.foregroundStyle(.secondary)
Text(meal.name ?? "")
.font(.body.weight(.medium))
.lineLimit(2)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(8)
.background(.quaternary.opacity(0.5), in: RoundedRectangle(cornerRadius: 8))
}
} else {
EmptySyncView()
}
}
}
.navigationTitle("MealMood")
}
}
struct EmptySyncView: View {
var body: some View {
VStack(spacing: 8) {
Image(systemName: "iphone.and.arrow.forward")
.font(.title3)
.foregroundStyle(.secondary)
// The payload arrives fully localized from the iPhone; the watch
// bundle carries no strings of its own, so this stays neutral.
Text(verbatim: "MealMood")
.font(.caption2)
.foregroundStyle(.secondary)
}
.padding(.top, 20)
}
}
+37
View File
@@ -0,0 +1,37 @@
import SwiftUI
struct WeekView: View {
@EnvironmentObject private var store: WatchWeekStore
var body: some View {
Group {
if let payload = store.payload {
List {
Section(payload.weekTitle) {
ForEach(payload.days, id: \.dayOfWeek) { day in
VStack(alignment: .leading, spacing: 3) {
Text(day.title)
.font(.caption.weight(.semibold))
.foregroundStyle(day.isToday
? Color(red: 1.0, green: 0.45, blue: 0.35)
: .primary)
ForEach(Array(day.meals.enumerated()), id: \.offset) { _, meal in
HStack(spacing: 4) {
Image(systemName: WatchWeekPayload.icon(for: meal.type))
.font(.system(size: 10))
.foregroundStyle(.secondary)
Text(meal.name ?? "")
.font(.caption2)
.lineLimit(1)
}
}
}
}
}
}
} else {
EmptySyncView()
}
}
}
}