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

168 lines
5.7 KiB
Swift

//
// SharedMoodIntent.swift
// Reflect
//
// Single VoteMoodIntent for all targets.
// Main app uses ForegroundContinuableIntent to run widget intents in app process.
//
// Add this file to ALL targets: Reflect (iOS), ReflectWidgetExtension, Reflect Watch App
//
import AppIntents
import SwiftUI
import SwiftData
import WidgetKit
import os.log
// MARK: - Vote Mood Intent
struct VoteMoodIntent: AppIntent {
static var title: LocalizedStringResource = "Vote Mood"
static var description = IntentDescription("Record your mood for today")
static var openAppWhenRun: Bool = false
@Parameter(title: "Mood")
var moodValue: Int
init() {
self.moodValue = 2
}
init(mood: Mood) {
self.moodValue = mood.rawValue
}
@MainActor
func perform() async throws -> some IntentResult {
let mood = Mood(rawValue: moodValue) ?? .average
#if os(watchOS)
// Watch: Send to iPhone via WatchConnectivity
let date = Date()
_ = WatchConnectivityManager.shared.sendMoodToPhone(mood: moodValue, date: date)
WidgetCenter.shared.reloadAllTimelines()
#elseif WIDGET_EXTENSION
// Widget: Save to shared container, main app handles side effects
let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding())
WidgetMoodSaver.save(mood: mood, date: votingDate)
WidgetCenter.shared.reloadAllTimelines()
#else
// Main app: Full logging with all side effects
let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding())
MoodLogger.shared.logMood(mood, for: votingDate, entryType: .widget)
let dateString = ISO8601DateFormatter().string(from: Calendar.current.startOfDay(for: votingDate))
GroupUserDefaults.groupDefaults.set(dateString, forKey: UserDefaultsStore.Keys.lastVotedDate.rawValue)
#endif
return .result()
}
}
// MARK: - Main App: Run widget intents in app process
#if !WIDGET_EXTENSION && !os(watchOS)
extension VoteMoodIntent: ForegroundContinuableIntent {}
#endif
// MARK: - Widget: Save to shared container
#if WIDGET_EXTENSION
enum WidgetMoodSaver {
private static let logger = Logger(subsystem: "com.88oakapps.reflect.widget", category: "WidgetMoodSaver")
private static var cachedContainer: ModelContainer?
@MainActor
static func save(mood: Mood, date: Date) {
do {
let container = try getOrCreateContainer()
try performSave(mood: mood, date: date, container: container)
} catch {
// Container may be stale or corrupted discard cache and retry once
logger.warning("First save attempt failed, retrying with fresh container: \(error.localizedDescription)")
cachedContainer = nil
do {
let container = try getOrCreateContainer()
try performSave(mood: mood, date: date, container: container)
} catch {
logger.error("Failed to save mood after retry: \(error.localizedDescription)")
}
}
}
private static func getOrCreateContainer() throws -> ModelContainer {
if let existing = cachedContainer {
return existing
}
let schema = Schema([MoodEntryModel.self])
let appGroupID = Constants.currentGroupShareId
guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID) else {
logger.error("App Group not available")
throw WidgetMoodSaverError.appGroupUnavailable
}
#if DEBUG
let storeURL = containerURL.appendingPathComponent("Reflect-Debug.store")
#else
let storeURL = containerURL.appendingPathComponent("Reflect.store")
#endif
let config = ModelConfiguration(schema: schema, url: storeURL, cloudKitDatabase: .none)
let container = try ModelContainer(for: schema, configurations: [config])
cachedContainer = container
return container
}
@MainActor
private static func performSave(mood: Mood, date: Date, container: ModelContainer) throws {
let context = container.mainContext
// Delete existing entry for this date
let startDate = Calendar.current.startOfDay(for: date)
let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate)!
let descriptor = FetchDescriptor<MoodEntryModel>(
predicate: #Predicate { entry in
entry.forDate >= startDate && entry.forDate < endDate
}
)
let existing = try context.fetch(descriptor)
if !existing.isEmpty {
for entry in existing {
context.delete(entry)
}
try context.save()
}
// Create new entry
let entry = MoodEntryModel(forDate: date, mood: mood, entryType: .widget)
context.insert(entry)
try context.save()
logger.info("Saved mood \(mood.rawValue) from widget")
// Note: The widget cannot run full side effects (HealthKit, streaks, analytics, etc.)
// because it runs in a separate extension process without access to MoodLogger.
// When the main app returns to the foreground, it calls
// MoodLogger.shared.processPendingSideEffects() to catch up on any side effects
// that were missed from widget or watch entries.
}
enum WidgetMoodSaverError: LocalizedError {
case appGroupUnavailable
var errorDescription: String? {
switch self {
case .appGroupUnavailable:
return "App Group container is not available"
}
}
}
}
#endif