Files
Reflect/Shared/Persisence/SharedModelContainer.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

134 lines
5.2 KiB
Swift

//
// 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 {
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
}
}