← All repositories

listless

A simple list app for Apple platforms

git clone https://code.inqk.net/listless.git


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

MListless/Sync/PersistenceController.swift | 16++++++++++++++--
MListlessiOS/ListlessiOSApp.swift | 35++++++++++++++++++++++++++++-------

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)
         }