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,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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user