diff --git a/Conjuga/Conjuga/ConjugaApp.swift b/Conjuga/Conjuga/ConjugaApp.swift index 18c9638..2fb6a04 100644 --- a/Conjuga/Conjuga/ConjugaApp.swift +++ b/Conjuga/Conjuga/ConjugaApp.swift @@ -217,26 +217,16 @@ struct ConjugaApp: App { } private static func makeLocalContainer(at url: URL) throws -> ModelContainer { + // Built from the single shared model list so the app and the widget + // extension always open the store with an identical schema. + let schema = Schema(SharedStore.localSchemaModels) let localConfig = ModelConfiguration( "local", - schema: Schema([ - Verb.self, VerbForm.self, IrregularSpan.self, - TenseGuide.self, CourseDeck.self, VocabCard.self, - TextbookChapter.self, - DownloadedVideo.self, - Book.self, BookChapter.self, - ]), + schema: schema, url: url, cloudKitDatabase: .none ) - return try ModelContainer( - for: Verb.self, VerbForm.self, IrregularSpan.self, - TenseGuide.self, CourseDeck.self, VocabCard.self, - TextbookChapter.self, - DownloadedVideo.self, - Book.self, BookChapter.self, - configurations: localConfig - ) + return try ModelContainer(for: schema, configurations: localConfig) } private static func localStoreIsUsable(container: ModelContainer) -> Bool { diff --git a/Conjuga/ConjugaWidget/CombinedWidget.swift b/Conjuga/ConjugaWidget/CombinedWidget.swift index ef24ef3..8639a6d 100644 --- a/Conjuga/ConjugaWidget/CombinedWidget.swift +++ b/Conjuga/ConjugaWidget/CombinedWidget.swift @@ -41,24 +41,19 @@ struct CombinedProvider: TimelineProvider { private func fetchWordOfDay(for date: Date) -> WordOfDay? { guard let localURL = SharedStore.localStoreURL() else { return nil } - // MUST declare all 7 local entities to match the main app's schema. - // Declaring a subset would cause SwiftData to destructively migrate the - // store on open, dropping the entities not listed here (this is how we - // previously lost all TextbookChapter rows on every widget refresh). + // Open the store with the SAME schema as the main app. A subset schema + // would make SwiftData destructively migrate the store on open and drop + // every unlisted table (this is how widget refreshes kept wiping the + // bundled Book rows, and TextbookChapter before them). + let schema = Schema(SharedStore.localSchemaModels) let config = ModelConfiguration( "local", - schema: Schema([ - Verb.self, VerbForm.self, IrregularSpan.self, - TenseGuide.self, CourseDeck.self, VocabCard.self, - TextbookChapter.self, - ]), + schema: schema, url: localURL, cloudKitDatabase: .none ) guard let container = try? ModelContainer( - for: Verb.self, VerbForm.self, IrregularSpan.self, - TenseGuide.self, CourseDeck.self, VocabCard.self, - TextbookChapter.self, + for: schema, configurations: config ) else { return nil } diff --git a/Conjuga/ConjugaWidget/WordOfDayWidget.swift b/Conjuga/ConjugaWidget/WordOfDayWidget.swift index c41c3be..1b490c8 100644 --- a/Conjuga/ConjugaWidget/WordOfDayWidget.swift +++ b/Conjuga/ConjugaWidget/WordOfDayWidget.swift @@ -32,24 +32,19 @@ struct WordOfDayProvider: TimelineProvider { private func fetchWordOfDay(for date: Date) -> WordOfDay? { guard let localURL = SharedStore.localStoreURL() else { return nil } - // MUST declare all 7 local entities to match the main app's schema. - // Declaring a subset would cause SwiftData to destructively migrate the - // store on open, dropping the entities not listed here (this is how we - // previously lost all TextbookChapter rows on every widget refresh). + // Open the store with the SAME schema as the main app. A subset schema + // would make SwiftData destructively migrate the store on open and drop + // every unlisted table (this is how widget refreshes kept wiping the + // bundled Book rows, and TextbookChapter before them). + let schema = Schema(SharedStore.localSchemaModels) let config = ModelConfiguration( "local", - schema: Schema([ - Verb.self, VerbForm.self, IrregularSpan.self, - TenseGuide.self, CourseDeck.self, VocabCard.self, - TextbookChapter.self, - ]), + schema: schema, url: localURL, cloudKitDatabase: .none ) guard let container = try? ModelContainer( - for: Verb.self, VerbForm.self, IrregularSpan.self, - TenseGuide.self, CourseDeck.self, VocabCard.self, - TextbookChapter.self, + for: schema, configurations: config ) else { return nil } diff --git a/Conjuga/SharedModels/Sources/SharedModels/SharedStore.swift b/Conjuga/SharedModels/Sources/SharedModels/SharedStore.swift index 50b2b83..0c5a710 100644 --- a/Conjuga/SharedModels/Sources/SharedModels/SharedStore.swift +++ b/Conjuga/SharedModels/Sources/SharedModels/SharedStore.swift @@ -23,4 +23,22 @@ public enum SharedStore { /// and hit the exact container used for seeding. @MainActor public static var localContainer: ModelContainer? + + /// The canonical model list for the local reference-data store. + /// + /// The main app AND the widget extension MUST build their `ModelContainer` + /// from this exact set. Opening the store with a *subset* schema makes + /// SwiftData destructively migrate it — silently dropping every table that + /// isn't listed. That is how widget refreshes repeatedly wiped the bundled + /// `Book`/`BookChapter` rows (and `TextbookChapter` before them). Keeping + /// one shared list means a newly-added model can't be forgotten in the + /// widget and quietly nuke its own data. + public static var localSchemaModels: [any PersistentModel.Type] { + [ + Verb.self, VerbForm.self, IrregularSpan.self, + TenseGuide.self, CourseDeck.self, VocabCard.self, + TextbookChapter.self, DownloadedVideo.self, + Book.self, BookChapter.self, + ] + } }