InvestmentTrackerApp/InvestmentTracker/Models/CoreDataStack.swift

145 lines
4.6 KiB
Swift

import CoreData
import CloudKit
class CoreDataStack: ObservableObject {
static let shared = CoreDataStack()
static let appGroupIdentifier = "group.com.yourteam.investmenttracker"
static let cloudKitContainerIdentifier = "iCloud.com.yourteam.investmenttracker"
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "InvestmentTracker")
// App Group store URL for sharing with widgets
guard let storeURL = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: Self.appGroupIdentifier)?
.appendingPathComponent("InvestmentTracker.sqlite") else {
fatalError("Unable to get App Group container URL")
}
let description = NSPersistentStoreDescription(url: storeURL)
// CloudKit configuration
description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
containerIdentifier: Self.cloudKitContainerIdentifier
)
// History tracking for sync
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { description, error in
if let error = error as NSError? {
// In production, handle this error appropriately
print("Core Data failed to load: \(error), \(error.userInfo)")
}
}
// Merge policy - remote changes win
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
// Performance optimizations
container.viewContext.undoManager = nil
container.viewContext.shouldDeleteInaccessibleFaults = true
// Listen for remote changes
NotificationCenter.default.addObserver(
self,
selector: #selector(processRemoteChanges),
name: .NSPersistentStoreRemoteChange,
object: container.persistentStoreCoordinator
)
return container
}()
var viewContext: NSManagedObjectContext {
return persistentContainer.viewContext
}
private init() {}
// MARK: - Save Context
func save() {
let context = viewContext
guard context.hasChanges else { return }
do {
try context.save()
} catch {
let nsError = error as NSError
print("Core Data save error: \(nsError), \(nsError.userInfo)")
}
}
// MARK: - Background Context
func newBackgroundContext() -> NSManagedObjectContext {
let context = persistentContainer.newBackgroundContext()
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
context.undoManager = nil
return context
}
func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
persistentContainer.performBackgroundTask(block)
}
// MARK: - Remote Change Handling
@objc private func processRemoteChanges(_ notification: Notification) {
// Process remote changes on main context
DispatchQueue.main.async { [weak self] in
self?.objectWillChange.send()
}
}
// MARK: - Widget Data Refresh
func refreshWidgetData() {
// Trigger widget timeline refresh when data changes
#if os(iOS)
if #available(iOS 14.0, *) {
import WidgetKit
WidgetCenter.shared.reloadAllTimelines()
}
#endif
}
}
// MARK: - Shared Container for Widgets
extension CoreDataStack {
static var sharedStoreURL: URL? {
return FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)?
.appendingPathComponent("InvestmentTracker.sqlite")
}
/// Creates a lightweight Core Data stack for widgets (read-only)
static func createWidgetContainer() -> NSPersistentContainer {
let container = NSPersistentContainer(name: "InvestmentTracker")
guard let storeURL = sharedStoreURL else {
fatalError("Unable to get shared store URL")
}
let description = NSPersistentStoreDescription(url: storeURL)
description.isReadOnly = true
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { _, error in
if let error = error {
print("Widget Core Data failed: \(error)")
}
}
return container
}
}