Files
Reflect/Shared/MoodLogger.swift
Trey t 224c00423a Add Apple Watch companion app with complications and WCSession sync
- Add watchOS app target with mood voting UI (5 mood buttons)
- Add WidgetKit complications (circular, corner, inline, rectangular)
- Add WatchConnectivityManager for bidirectional sync between iOS and watch
- iOS app acts as central coordinator - all mood logging flows through MoodLogger
- Watch votes send to iPhone via WCSession, iPhone logs and notifies watch back
- Widget votes use openAppWhenRun=true to run MoodLogger in main app process
- Add #if !os(watchOS) guards to Mood.swift and Random.swift for compatibility
- Update SKStoreReviewController to AppStore.requestReview (iOS 18 deprecation fix)
- Watch reads user's moodImages preference from GroupUserDefaults for emoji style

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 17:19:17 -06:00

96 lines
3.3 KiB
Swift

//
// MoodLogger.swift
// Feels
//
// Centralized mood logging service that handles all side effects
//
import Foundation
import WidgetKit
/// Centralized service for logging moods with all associated side effects.
/// All mood entry points should use this service to ensure consistent behavior.
@MainActor
final class MoodLogger {
static let shared = MoodLogger()
private init() {}
/// Log a mood entry with all associated side effects.
/// This is the single source of truth for mood logging in the app.
///
/// - Parameters:
/// - mood: The mood to log
/// - date: The date for the mood entry
/// - entryType: The source of the mood entry (header, widget, siri, etc.)
/// - syncHealthKit: Whether to sync to HealthKit (default true, but widget can't access HealthKit)
/// - updateTips: Whether to update TipKit parameters (default true, but widget can't access TipKit)
func logMood(
_ mood: Mood,
for date: Date,
entryType: EntryType,
syncHealthKit: Bool = true,
updateTips: Bool = true
) {
// 1. Add mood entry to data store
DataController.shared.add(mood: mood, forDate: date, entryType: entryType)
// Skip side effects for placeholder/missing moods
guard mood != .missing && mood != .placeholder else { return }
// 2. Sync to HealthKit if enabled and requested
if syncHealthKit {
let healthKitEnabled = GroupUserDefaults.groupDefaults.bool(forKey: UserDefaultsStore.Keys.healthKitEnabled.rawValue)
if healthKitEnabled {
Task {
try? await HealthKitManager.shared.saveMood(mood, for: date)
}
}
}
// 3. Calculate current streak for Live Activity and TipKit
let streak = calculateCurrentStreak()
// 4. Update Live Activity
LiveActivityManager.shared.updateActivity(streak: streak, mood: mood)
LiveActivityScheduler.shared.scheduleForNextDay()
// 5. Update TipKit parameters if requested
if updateTips {
TipsManager.shared.onMoodLogged()
TipsManager.shared.updateStreak(streak)
}
// 6. Request app review at moments of delight
ReviewRequestManager.shared.onMoodLogged(streak: streak)
// 7. Reload widgets
WidgetCenter.shared.reloadAllTimelines()
// 8. Notify watch to refresh complications
WatchConnectivityManager.shared.notifyWatchToReload()
}
/// Calculate the current mood streak
private func calculateCurrentStreak() -> Int {
var streak = 0
var checkDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding())
while true {
let dayStart = Calendar.current.startOfDay(for: checkDate)
let dayEnd = Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: dayStart)!
let entry = DataController.shared.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first
if let entry = entry, entry.mood != .missing && entry.mood != .placeholder {
streak += 1
checkDate = Calendar.current.date(byAdding: .day, value: -1, to: checkDate)!
} else {
break
}
}
return streak
}
}