Fix widget wiping the Books tables on every refresh
The widget extension opened the shared local SwiftData store with a 7-entity schema while the app's store has 10. SwiftData treats the smaller schema as a migration and destructively drops the unlisted tables — so every widget refresh deleted the bundled Book/BookChapter rows (and DownloadedVideo), which is why books vanished after reinstalls. Introduce SharedStore.localSchemaModels as the single source of truth for the local schema and build the app and both widget containers from it, so app and widget can no longer drift apart. The same class of bug hit TextbookChapter previously; a shared list prevents a third recurrence. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -217,26 +217,16 @@ struct ConjugaApp: App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func makeLocalContainer(at url: URL) throws -> ModelContainer {
|
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(
|
let localConfig = ModelConfiguration(
|
||||||
"local",
|
"local",
|
||||||
schema: Schema([
|
schema: schema,
|
||||||
Verb.self, VerbForm.self, IrregularSpan.self,
|
|
||||||
TenseGuide.self, CourseDeck.self, VocabCard.self,
|
|
||||||
TextbookChapter.self,
|
|
||||||
DownloadedVideo.self,
|
|
||||||
Book.self, BookChapter.self,
|
|
||||||
]),
|
|
||||||
url: url,
|
url: url,
|
||||||
cloudKitDatabase: .none
|
cloudKitDatabase: .none
|
||||||
)
|
)
|
||||||
return try ModelContainer(
|
return try ModelContainer(for: schema, configurations: localConfig)
|
||||||
for: Verb.self, VerbForm.self, IrregularSpan.self,
|
|
||||||
TenseGuide.self, CourseDeck.self, VocabCard.self,
|
|
||||||
TextbookChapter.self,
|
|
||||||
DownloadedVideo.self,
|
|
||||||
Book.self, BookChapter.self,
|
|
||||||
configurations: localConfig
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func localStoreIsUsable(container: ModelContainer) -> Bool {
|
private static func localStoreIsUsable(container: ModelContainer) -> Bool {
|
||||||
|
|||||||
@@ -41,24 +41,19 @@ struct CombinedProvider: TimelineProvider {
|
|||||||
private func fetchWordOfDay(for date: Date) -> WordOfDay? {
|
private func fetchWordOfDay(for date: Date) -> WordOfDay? {
|
||||||
guard let localURL = SharedStore.localStoreURL() else { return nil }
|
guard let localURL = SharedStore.localStoreURL() else { return nil }
|
||||||
|
|
||||||
// MUST declare all 7 local entities to match the main app's schema.
|
// Open the store with the SAME schema as the main app. A subset schema
|
||||||
// Declaring a subset would cause SwiftData to destructively migrate the
|
// would make SwiftData destructively migrate the store on open and drop
|
||||||
// store on open, dropping the entities not listed here (this is how we
|
// every unlisted table (this is how widget refreshes kept wiping the
|
||||||
// previously lost all TextbookChapter rows on every widget refresh).
|
// bundled Book rows, and TextbookChapter before them).
|
||||||
|
let schema = Schema(SharedStore.localSchemaModels)
|
||||||
let config = ModelConfiguration(
|
let config = ModelConfiguration(
|
||||||
"local",
|
"local",
|
||||||
schema: Schema([
|
schema: schema,
|
||||||
Verb.self, VerbForm.self, IrregularSpan.self,
|
|
||||||
TenseGuide.self, CourseDeck.self, VocabCard.self,
|
|
||||||
TextbookChapter.self,
|
|
||||||
]),
|
|
||||||
url: localURL,
|
url: localURL,
|
||||||
cloudKitDatabase: .none
|
cloudKitDatabase: .none
|
||||||
)
|
)
|
||||||
guard let container = try? ModelContainer(
|
guard let container = try? ModelContainer(
|
||||||
for: Verb.self, VerbForm.self, IrregularSpan.self,
|
for: schema,
|
||||||
TenseGuide.self, CourseDeck.self, VocabCard.self,
|
|
||||||
TextbookChapter.self,
|
|
||||||
configurations: config
|
configurations: config
|
||||||
) else { return nil }
|
) else { return nil }
|
||||||
|
|
||||||
|
|||||||
@@ -32,24 +32,19 @@ struct WordOfDayProvider: TimelineProvider {
|
|||||||
private func fetchWordOfDay(for date: Date) -> WordOfDay? {
|
private func fetchWordOfDay(for date: Date) -> WordOfDay? {
|
||||||
guard let localURL = SharedStore.localStoreURL() else { return nil }
|
guard let localURL = SharedStore.localStoreURL() else { return nil }
|
||||||
|
|
||||||
// MUST declare all 7 local entities to match the main app's schema.
|
// Open the store with the SAME schema as the main app. A subset schema
|
||||||
// Declaring a subset would cause SwiftData to destructively migrate the
|
// would make SwiftData destructively migrate the store on open and drop
|
||||||
// store on open, dropping the entities not listed here (this is how we
|
// every unlisted table (this is how widget refreshes kept wiping the
|
||||||
// previously lost all TextbookChapter rows on every widget refresh).
|
// bundled Book rows, and TextbookChapter before them).
|
||||||
|
let schema = Schema(SharedStore.localSchemaModels)
|
||||||
let config = ModelConfiguration(
|
let config = ModelConfiguration(
|
||||||
"local",
|
"local",
|
||||||
schema: Schema([
|
schema: schema,
|
||||||
Verb.self, VerbForm.self, IrregularSpan.self,
|
|
||||||
TenseGuide.self, CourseDeck.self, VocabCard.self,
|
|
||||||
TextbookChapter.self,
|
|
||||||
]),
|
|
||||||
url: localURL,
|
url: localURL,
|
||||||
cloudKitDatabase: .none
|
cloudKitDatabase: .none
|
||||||
)
|
)
|
||||||
guard let container = try? ModelContainer(
|
guard let container = try? ModelContainer(
|
||||||
for: Verb.self, VerbForm.self, IrregularSpan.self,
|
for: schema,
|
||||||
TenseGuide.self, CourseDeck.self, VocabCard.self,
|
|
||||||
TextbookChapter.self,
|
|
||||||
configurations: config
|
configurations: config
|
||||||
) else { return nil }
|
) else { return nil }
|
||||||
|
|
||||||
|
|||||||
@@ -23,4 +23,22 @@ public enum SharedStore {
|
|||||||
/// and hit the exact container used for seeding.
|
/// and hit the exact container used for seeding.
|
||||||
@MainActor
|
@MainActor
|
||||||
public static var localContainer: ModelContainer?
|
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,
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user