// // SharedModelContainer.swift // Reflect // // Factory for creating ModelContainer shared between main app and widget extension. // import Foundation import SwiftData import os.log /// Errors that can occur when creating the shared model container enum SharedModelContainerError: LocalizedError { case appGroupNotAvailable(String) case modelContainerCreationFailed(Error) var errorDescription: String? { switch self { case .appGroupNotAvailable(let groupID): return "App Group container not available for: \(groupID)" case .modelContainerCreationFailed(let error): return "Failed to create ModelContainer: \(error.localizedDescription)" } } } enum SharedModelContainer { private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.88oakapps.reflect", category: "SharedModelContainer") /// Indicates whether the app is running with in-memory storage due to a failed App Group container. /// When `true`, all data will be lost on app restart. static private(set) var isUsingInMemoryFallback = false /// Creates a ModelContainer with the appropriate configuration for app group sharing /// - Parameter useCloudKit: Whether to enable CloudKit sync (defaults to true) /// - Returns: Configured ModelContainer /// - Throws: SharedModelContainerError if creation fails static func create(useCloudKit: Bool = true) throws -> ModelContainer { // When UI testing, use in-memory storage for parallel test isolation. // Each test process gets its own empty container — no shared on-disk state. // Check ProcessInfo directly to avoid depending on UITestMode (not in widget targets). if ProcessInfo.processInfo.arguments.contains("--ui-testing") { let schema = Schema([MoodEntryModel.self]) let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true, cloudKitDatabase: .none) return try ModelContainer(for: schema, configurations: [config]) } let schema = Schema([MoodEntryModel.self]) let storeURL = try Self.storeURL let configuration: ModelConfiguration if useCloudKit { // CloudKit-enabled configuration configuration = ModelConfiguration( schema: schema, url: storeURL, cloudKitDatabase: .private(cloudKitContainerID) ) } else { // Local-only configuration configuration = ModelConfiguration( schema: schema, url: storeURL, cloudKitDatabase: .none ) } do { return try ModelContainer(for: schema, configurations: [configuration]) } catch { logger.error("Failed to create ModelContainer: \(error.localizedDescription)") throw SharedModelContainerError.modelContainerCreationFailed(error) } } /// Creates a ModelContainer, falling back to in-memory storage if shared container fails /// - Parameter useCloudKit: Whether to enable CloudKit sync (defaults to true) /// - Returns: Configured ModelContainer (shared or in-memory fallback) static func createWithFallback(useCloudKit: Bool = true) -> ModelContainer { do { return try create(useCloudKit: useCloudKit) } catch { logger.warning("Falling back to in-memory storage due to: \(error.localizedDescription)") logger.critical("App is using in-memory storage — all mood data will be lost on restart. App Group container failed: \(error.localizedDescription)") #if DEBUG assertionFailure("SharedModelContainer fell back to in-memory storage. App Group container is unavailable: \(error.localizedDescription)") #endif isUsingInMemoryFallback = true // Fall back to in-memory storage let schema = Schema([MoodEntryModel.self]) let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true) do { return try ModelContainer(for: schema, configurations: [config]) } catch { // This should never happen with in-memory storage, but handle it gracefully logger.critical("Failed to create even in-memory ModelContainer: \(error.localizedDescription)") preconditionFailure("Unable to create ModelContainer: \(error)") } } } /// The URL for the SwiftData store in the shared app group container /// - Throws: SharedModelContainerError if app group is not available static var storeURL: URL { get throws { guard let containerURL = FileManager.default.containerURL( forSecurityApplicationGroupIdentifier: appGroupID ) else { logger.error("App Group container not available for: \(appGroupID)") throw SharedModelContainerError.appGroupNotAvailable(appGroupID) } return containerURL.appendingPathComponent(storeFileName) } } /// App Group identifier based on build configuration static var appGroupID: String { #if DEBUG return Constants.groupShareIdDebug #else return Constants.groupShareId #endif } /// CloudKit container identifier based on build configuration static var cloudKitContainerID: String { #if DEBUG return "iCloud.com.88oakapps.reflect.debug" #else return "iCloud.com.88oakapps.reflect" #endif } /// Store file name based on build configuration static var storeFileName: String { #if DEBUG return "Reflect-Debug.store" #else return "Reflect.store" #endif } }