commit f9f00df839ad3190df2c5cec036f77658307701b
parent ac3ad1528bdbd984114498e03d53f5a0ae012087
Author: Michael Camilleri <[email protected]>
Date: Tue, 31 Mar 2026 10:09:41 +0900
Move tutorial into a separate view to avoid crashes
The tutorial's PersistenceController was a local variable in a computed
property, so it was deallocated during the SwiftUI transition when the tutorial
was dismissed. The @FetchRequest could then crash accessing the dead context.
This commit extracts the tutorial into a TutorialListView struct that owns its
PersistenceController via @State, tying its lifetime to the view's. A single
static NSManagedObjectModel is also shared across all containers so the
tutorial and main PersistenceControllers don't register competing
NSEntityDescriptions for TaskItem.
Co-Authored-By: Claude 4.6 Opus <[email protected]>
Diffstat:
2 files changed, 42 insertions(+), 9 deletions(-)
diff --git a/Listless/Sync/PersistenceController.swift b/Listless/Sync/PersistenceController.swift
@@ -50,6 +50,16 @@ private final class UpdatedAtMergePolicy: NSMergePolicy {
final class PersistenceController {
static let shared = PersistenceController()
+ private static let model: NSManagedObjectModel = {
+ let bundle = Bundle(for: PersistenceController.self)
+ guard let url = bundle.url(forResource: "Listless", withExtension: "momd"),
+ let model = NSManagedObjectModel(contentsOf: url)
+ else {
+ fatalError("Failed to load Core Data model")
+ }
+ return model
+ }()
+
let container: NSPersistentContainer
let syncMonitor: CloudKitSyncMonitor
@@ -66,10 +76,12 @@ final class PersistenceController {
let tempURL = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("sqlite")
- container = NSPersistentContainer(name: "Listless")
+ container = NSPersistentContainer(
+ name: "Listless", managedObjectModel: Self.model)
container.persistentStoreDescriptions.first?.url = tempURL
} else {
- container = NSPersistentCloudKitContainer(name: "Listless")
+ container = NSPersistentCloudKitContainer(
+ name: "Listless", managedObjectModel: Self.model)
// Configure CloudKit sync
guard let description = container.persistentStoreDescriptions.first else {
fatalError("Failed to retrieve persistent store description")
diff --git a/ListlessiOS/ListlessiOSApp.swift b/ListlessiOS/ListlessiOSApp.swift
@@ -81,7 +81,7 @@ struct ListlessiOSApp: App {
if didCompleteTutorial {
mainListView
} else {
- tutorialListView
+ TutorialListView { didCompleteTutorial = true }
}
}
}
@@ -106,19 +106,40 @@ struct ListlessiOSApp: App {
}
}
- private var tutorialListView: some View {
- let pc = PersistenceController(inMemory: true)
- let store = ItemStore(persistenceController: pc)
+ private func applyAppearanceMode(_ mode: Int) {
+ let style: UIUserInterfaceStyle = switch mode {
+ case 1: .light
+ case 2: .dark
+ default: .unspecified
+ }
+ for scene in UIApplication.shared.connectedScenes {
+ guard let windowScene = scene as? UIWindowScene else { continue }
+ for window in windowScene.windows {
+ window.overrideUserInterfaceStyle = style
+ }
+ }
+ }
+}
+
+// MARK: - Tutorial
+
+struct TutorialListView: View {
+ @AppStorage("appearanceMode") private var appearanceMode = 0
+ @State private var persistenceController = PersistenceController(inMemory: true)
+ var onFinishTutorial: () -> Void
+
+ var body: some View {
+ let store = ItemStore(persistenceController: persistenceController)
TutorialSeeder.seed(store: store)
return ItemListView(
store: store,
- syncMonitor: pc.syncMonitor,
- onFinishTutorial: { didCompleteTutorial = true }
+ syncMonitor: persistenceController.syncMonitor,
+ onFinishTutorial: onFinishTutorial
)
.safeAreaInset(edge: .top) {
Color.clear.frame(height: 8)
}
- .environment(\.managedObjectContext, pc.viewContext)
+ .environment(\.managedObjectContext, persistenceController.viewContext)
.onChange(of: appearanceMode, initial: true) { _, newValue in
applyAppearanceMode(newValue)
}