listless

A simple list app for Apple platforms
Log | Files | Refs | README | LICENSE

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