- Bundle IDs: com.tt.ifeel* → com.tt.feels* - App Groups: group.com.tt.ifeel* → group.com.tt.feels* - iCloud containers: iCloud.com.tt.ifeel* → iCloud.com.tt.feels* - IAP product IDs: com.tt.ifeel.IAP.* → com.tt.feels.IAP.* - URLs: ifeels.app → feels.app - Logger subsystems and dispatch queues - Product names and display names 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
125 lines
4.5 KiB
Swift
125 lines
4.5 KiB
Swift
//
|
|
// SharedModelContainer.swift
|
|
// Feels
|
|
//
|
|
// 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.tt.feels", category: "SharedModelContainer")
|
|
|
|
/// 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)")
|
|
// 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.tt.feelsDebug"
|
|
#else
|
|
return "iCloud.com.tt.feels"
|
|
#endif
|
|
}
|
|
|
|
/// Store file name based on build configuration
|
|
static var storeFileName: String {
|
|
#if DEBUG
|
|
return "Feels-Debug.store"
|
|
#else
|
|
return "Feels.store"
|
|
#endif
|
|
}
|
|
}
|