Files
Reflect/Shared/Persisence/DataController.swift
Trey t 0442eab1f8 Rebrand entire project from Feels to Reflect
Complete rename across all bundle IDs, App Groups, CloudKit containers,
StoreKit product IDs, data store filenames, URL schemes, logger subsystems,
Swift identifiers, user-facing strings (7 languages), file names, directory
names, Xcode project, schemes, assets, and documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 11:47:16 -06:00

109 lines
3.2 KiB
Swift

//
// 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<MoodEntryModel>(
sortBy: [SortDescriptor(\.forDate, order: .forward)]
)
descriptor.fetchLimit = 1
return try? modelContext.fetch(descriptor).first
}
var latestEntry: MoodEntryModel? {
var descriptor = FetchDescriptor<MoodEntryModel>(
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")
}
}