2.0: CloudKit private-database sync replaces KV snapshot sync
Foundation for 2.1 family sharing (CKShare needs these models + container). SwiftData cannot share across Apple IDs today, so 2.0 ships true multi-device sync for the same account instead: - Models made CloudKit-compatible: dropped @Attribute(.unique) on all 5, inline defaults on every attribute, WeekPlan.slots stored as optional relationship (name preserved → lightweight migration) with non-optional slotList facade; ~73 call sites renamed. - Container: cloudKitDatabase .automatic, falling back to the local-only store when CloudKit is unavailable; failures recorded to Crashlytics. - Entitlements: iCloud CloudKit service (container already existed); remote-notification background mode for push-driven sync. - Legacy KV snapshot sync (ICloudSyncService) stays inert when CloudKit is active — kept only for 1.x devices. - DeduplicationService collapses cross-device duplicates deterministically on launch (settings singleton, default tags with tagId remapping, same-week plans). - Note: AppSettings.isPremium now syncs across same-Apple-ID devices; StoreKit (PremiumSyncService + Transaction.updates) remains the source of truth and reconciles on every launch/foreground. All 21 unit tests pass, including ICloudSyncPremiumIsolationTests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkanrydYtrme8wipTzWssG
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
// CloudKit-compatible (2.0): no unique constraints, inline defaults everywhere.
|
||||
@Model
|
||||
final class AppSettings {
|
||||
var mealWindows: String // "dinnerOnly", "lunchOnly", "both"
|
||||
var mealWindows: String = "dinnerOnly" // legacy: "dinnerOnly", "lunchOnly", "both"
|
||||
var includeWeekends: Bool = true
|
||||
var language: String // "spanish", "english"
|
||||
var language: String = "system"
|
||||
|
||||
/// Comma-separated MealType raw values, in chronological order (2.0).
|
||||
/// nil = derive from the legacy `mealWindows` field (pre-2.0 installs).
|
||||
@@ -14,12 +15,12 @@ final class AppSettings {
|
||||
var calendarId: String?
|
||||
var syncEnabled: Bool = false
|
||||
var syncMode: String? // "weekComplete", "manual"
|
||||
var lunchTime: Date
|
||||
var dinnerTime: Date
|
||||
var lunchTime: Date = AppSettings.defaultTime(hour: 14)
|
||||
var dinnerTime: Date = AppSettings.defaultTime(hour: 21)
|
||||
var breakfastTime: Date? // optional: pre-2.0 stores lack it
|
||||
var snackTime: Date?
|
||||
var eventDuration: Int
|
||||
var eventPrefix: String
|
||||
var eventDuration: Int = 60
|
||||
var eventPrefix: String = "🍽️"
|
||||
var reminderMinutesBefore: Int?
|
||||
var iCloudSyncEnabled: Bool?
|
||||
var weekExportStyle: String?
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
// CloudKit-compatible (2.0): no unique constraints, inline defaults everywhere.
|
||||
@Model
|
||||
final class Dish {
|
||||
@Attribute(.unique) var id: UUID
|
||||
var name: String
|
||||
var id: UUID = UUID()
|
||||
var name: String = ""
|
||||
var descriptionText: String?
|
||||
var tagIds: [UUID]
|
||||
var createdAt: Date
|
||||
var tagIds: [UUID] = []
|
||||
var createdAt: Date = Date()
|
||||
var isPriority: Bool = false
|
||||
|
||||
/// Optional ingredient lines (one per entry, e.g. "200g spaghetti").
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
// CloudKit-compatible (2.0): no unique constraints, inline defaults everywhere.
|
||||
@Model
|
||||
final class MealSlot {
|
||||
@Attribute(.unique) var id: UUID
|
||||
var dayOfWeek: Int // 0=Monday, 6=Sunday
|
||||
var mealType: String // "lunch", "dinner"
|
||||
var id: UUID = UUID()
|
||||
var dayOfWeek: Int = 0 // 0=Monday, 6=Sunday
|
||||
var mealType: String = MealType.dinner.rawValue
|
||||
var dishId: UUID?
|
||||
var calendarEventId: String?
|
||||
var isRuleOverridden: Bool = false
|
||||
|
||||
@@ -4,15 +4,16 @@ import SwiftData
|
||||
/// One line of the weekly shopping list. Items are either derived from a
|
||||
/// planned dish's ingredients (`dishId` set) or added free-hand by the user
|
||||
/// (`dishId == nil`). Check-off state persists across app launches.
|
||||
// CloudKit-compatible (2.0): no unique constraints, inline defaults everywhere.
|
||||
@Model
|
||||
final class ShoppingItem {
|
||||
@Attribute(.unique) var id: UUID
|
||||
var weekStartDate: Date
|
||||
var title: String
|
||||
var id: UUID = UUID()
|
||||
var weekStartDate: Date = Date()
|
||||
var title: String = ""
|
||||
var dishId: UUID?
|
||||
var isChecked: Bool = false
|
||||
var sortOrder: Int = 0
|
||||
var createdAt: Date
|
||||
var createdAt: Date = Date()
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
// CloudKit-compatible (2.0): no unique constraints, inline defaults everywhere.
|
||||
@Model
|
||||
final class Tag {
|
||||
@Attribute(.unique) var id: UUID
|
||||
var name: String
|
||||
var nameEN: String
|
||||
var color: String
|
||||
var id: UUID = UUID()
|
||||
var name: String = ""
|
||||
var nameEN: String = ""
|
||||
var color: String = ""
|
||||
var maxPerWeek: Int?
|
||||
var noConsecutive: Bool = false
|
||||
var noDuplicateInDay: Bool = false
|
||||
var mealTypeRestriction: String? // "lunch", "dinner", nil
|
||||
var dayRestriction: String? // nil = any day, "weekdays" = Mon-Fri, "weekend" = Sat-Sun
|
||||
var isDefault: Bool = true
|
||||
var sortOrder: Int
|
||||
var sortOrder: Int = 0
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
// CloudKit-compatible (2.0): no unique constraints, inline defaults everywhere,
|
||||
// and the relationship stored as optional (CloudKit requires it). `slots` keeps
|
||||
// its name so existing stores lightweight-migrate; use `slotList` in code.
|
||||
@Model
|
||||
final class WeekPlan {
|
||||
@Attribute(.unique) var id: UUID
|
||||
var weekStartDate: Date
|
||||
var createdAt: Date
|
||||
var updatedAt: Date
|
||||
var id: UUID = UUID()
|
||||
var weekStartDate: Date = Date()
|
||||
var createdAt: Date = Date()
|
||||
var updatedAt: Date = Date()
|
||||
var userRating: Int = 0 // 0 = unrated, 1 = liked, -1 = disliked
|
||||
|
||||
@Relationship(deleteRule: .cascade)
|
||||
var slots: [MealSlot]
|
||||
var slots: [MealSlot]? = []
|
||||
|
||||
/// Non-optional facade over the CloudKit-required optional relationship.
|
||||
var slotList: [MealSlot] {
|
||||
get { slots ?? [] }
|
||||
set { slots = newValue }
|
||||
}
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
|
||||
Reference in New Issue
Block a user