// // DataController.swift // Reflect // // SwiftData controller replacing Core Data PersistenceController. // import SwiftData import SwiftUI import os.log @MainActor final class DataController: ObservableObject { private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.88oakapps.reflect", category: "DataController") static let shared = DataController() private(set) var container: ModelContainer var modelContext: ModelContext { container.mainContext } // Listeners for data changes (keeping existing pattern) typealias DataListenerToken = UUID private var dataListeners: [DataListenerToken: () -> Void] = [:] // Computed properties for earliest/latest entries var earliestEntry: MoodEntryModel? { var descriptor = FetchDescriptor( sortBy: [SortDescriptor(\.forDate, order: .forward)] ) descriptor.fetchLimit = 1 return try? modelContext.fetch(descriptor).first } var latestEntry: MoodEntryModel? { var descriptor = FetchDescriptor( sortBy: [SortDescriptor(\.forDate, order: .reverse)] ) descriptor.fetchLimit = 1 return try? modelContext.fetch(descriptor).first } init(container: ModelContainer? = nil) { self.container = container ?? SharedModelContainer.createWithFallback(useCloudKit: true) } // MARK: - Listener Management @discardableResult func addNewDataListener(closure: @escaping (() -> Void)) -> DataListenerToken { let token = DataListenerToken() dataListeners[token] = closure return token } func removeDataListener(token: DataListenerToken) { dataListeners.removeValue(forKey: token) } @discardableResult func saveAndRunDataListeners() -> Bool { let success = save() if success { for closure in dataListeners.values { closure() } } return success } @discardableResult func save() -> Bool { guard modelContext.hasChanges else { return true } do { try modelContext.save() return true } catch { Self.logger.error("Failed to save context, retrying: \(error.localizedDescription)") do { try modelContext.save() return true } catch { Self.logger.critical("Failed to save context after retry: \(error.localizedDescription)") return false } } } /// Refresh data from disk to pick up changes made by extensions (widget/watch). /// Call this when app becomes active. func refreshFromDisk() { // SwiftData doesn't have a direct "refresh from disk" API. // We achieve this by: // 1. Rolling back any unsaved changes (ensures clean state) // 2. Triggering listeners to re-fetch data (which will read from disk) modelContext.rollback() // Notify listeners to re-fetch their data for closure in dataListeners.values { closure() } Self.logger.debug("Refreshed data from disk") } }