a049e4f74b
El iPhone marcaba isToday al construir el snapshot y el reloj se creia esa marca para siempre. Como el snapshot sobrevive en el app group, un sabado seguia enseñando la cena del miercoles: el dia que era cuando se genero. Ahora cada dia viaja con su fecha real y el reloj busca hoy cuando dibuja. Si el snapshot es de otra semana no hay "hoy" que enseñar, asi que en vez de colar la comida de otro dia lo dice y pide abrir el iPhone. Para snapshots guardados por versiones anteriores, que no traen fecha, solo se confia en el indice del dia mientras el propio snapshot sea de esta semana. Ademas el reloj pide datos al abrirse y el iPhone, en vez de responder con lo ultimo que cacheo, reconstruye la semana desde el store — que es la otra mitad del "no se actualiza": el cache podia ser de hace dias. Y cuando no hay nada planificado se dice, que antes era un guion indistinguible de una comida marcada como no planificada: "Hoy no hay nada planificado" para el dia entero y "Sin planificar" por comida, en los 6 idiomas (el bundle del reloj no tiene cadenas propias, viajan en el envio). Refs #34, #35 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013su1ttRiMeMYxkZJ1Y3246
100 lines
3.8 KiB
Swift
100 lines
3.8 KiB
Swift
import SwiftUI
|
|
import SwiftData
|
|
import UIKit
|
|
import FirebaseCore
|
|
import FirebaseCrashlytics
|
|
#if canImport(GoogleMobileAds)
|
|
import GoogleMobileAds
|
|
#endif
|
|
|
|
/// Registers for remote notifications so CloudKit (the NSPersistentCloudKitContainer
|
|
/// behind SwiftData) receives its silent-push subscriptions and imports changes
|
|
/// in near-real-time across devices. Silent pushes are coalesced by the system,
|
|
/// so battery cost is negligible — no polling.
|
|
final class AppDelegate: NSObject, UIApplicationDelegate {
|
|
func application(_ application: UIApplication,
|
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
|
application.registerForRemoteNotifications()
|
|
return true
|
|
}
|
|
|
|
func application(_ application: UIApplication,
|
|
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
|
|
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
|
// CloudKit silent push: SwiftData's mirror imports on its own once woken;
|
|
// report new data so the system keeps delivering sync pushes.
|
|
completionHandler(.newData)
|
|
}
|
|
}
|
|
|
|
/// Whether the store is syncing through CloudKit this launch. When true, the
|
|
/// legacy iCloud KV snapshot sync must stay inert (both would fight).
|
|
enum CloudSyncRuntime {
|
|
// Written exactly once during app start (before any concurrency), then
|
|
// read-only for the rest of the launch.
|
|
nonisolated(unsafe) static var isCloudKitActive = false
|
|
}
|
|
|
|
@main
|
|
struct MealMoodApp: App {
|
|
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
|
|
|
private let modelContainer: ModelContainer = {
|
|
let schema = Schema([
|
|
AppSettings.self,
|
|
Tag.self,
|
|
Dish.self,
|
|
WeekPlan.self,
|
|
MealSlot.self,
|
|
ShoppingItem.self
|
|
])
|
|
|
|
// 2.0: CloudKit private-database sync (multi-device, same Apple ID).
|
|
// Falls back to the local-only store when CloudKit isn't available
|
|
// (signed-out iCloud, missing entitlement in dev builds, etc.).
|
|
let cloudConfiguration = ModelConfiguration(cloudKitDatabase: .automatic)
|
|
do {
|
|
let container = try ModelContainer(for: schema, configurations: [cloudConfiguration])
|
|
CloudSyncRuntime.isCloudKitActive = true
|
|
return container
|
|
} catch {
|
|
CrashlyticsService.record(error, context: "cloudkit_container")
|
|
print("CloudKit container unavailable, using local store: \(error)")
|
|
}
|
|
|
|
let configuration = ModelConfiguration(cloudKitDatabase: .none)
|
|
do {
|
|
return try ModelContainer(for: schema, configurations: [configuration])
|
|
} catch {
|
|
// Migration failed — reset the store to avoid a crash loop
|
|
let storeURL = configuration.url
|
|
for ext in ["", "-shm", "-wal"] {
|
|
try? FileManager.default.removeItem(at: URL(fileURLWithPath: storeURL.path + ext))
|
|
}
|
|
do {
|
|
return try ModelContainer(for: schema, configurations: [configuration])
|
|
} catch {
|
|
fatalError("Cannot create ModelContainer after reset: \(error)")
|
|
}
|
|
}
|
|
}()
|
|
|
|
init() {
|
|
FirebaseApp.configure()
|
|
// The watch can ask for the week while the app is in the background, and
|
|
// answering that needs the store.
|
|
WatchSyncService.shared.configure(container: modelContainer)
|
|
AnalyticsService.applyCollectionPolicy()
|
|
#if canImport(GoogleMobileAds)
|
|
GADMobileAds.sharedInstance().start(completionHandler: nil)
|
|
#endif
|
|
}
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
ContentView()
|
|
}
|
|
.modelContainer(modelContainer)
|
|
}
|
|
}
|