Add Apple platform features and UX improvements

- Add HealthKit State of Mind sync for mood entries
- Add Live Activity with streak display and rating time window
- Add App Shortcuts/Siri integration for voice mood logging
- Add TipKit hints for feature discovery
- Add centralized MoodLogger for consistent side effects
- Add reminder time setting in Settings with time picker
- Fix duplicate notifications when changing reminder time
- Fix Live Activity streak showing 0 when not yet rated today
- Fix slow tap response in entry detail mood selection
- Update widget timeline to refresh at rating time
- Sync widgets when reminder time changes

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-19 17:21:55 -06:00
parent e123df1790
commit 440b04159e
27 changed files with 1577 additions and 81 deletions

View File

@@ -9,6 +9,7 @@ import SwiftUI
import CloudKitSyncMonitor
import UniformTypeIdentifiers
import StoreKit
import TipKit
// MARK: - Settings Content View (for use in SettingsTabView)
struct SettingsContentView: View {
@@ -16,6 +17,7 @@ struct SettingsContentView: View {
@State private var showOnboarding = false
@State private var showExportView = false
@State private var showReminderTimePicker = false
@StateObject private var healthService = HealthService.shared
@AppStorage(UserDefaultsStore.Keys.deleteEnable.rawValue, store: GroupUserDefaults.groupDefaults) private var deleteEnabled = true
@@ -33,6 +35,7 @@ struct SettingsContentView: View {
// Settings section
settingsSectionHeader
reminderTimeButton
canDelete
showOnboardingButton
@@ -64,11 +67,59 @@ struct SettingsContentView: View {
includedDays: []
))
}
.sheet(isPresented: $showReminderTimePicker) {
ReminderTimePickerView()
}
.onAppear(perform: {
EventLogger.log(event: "show_settings_view")
TipsManager.shared.onSettingsViewed()
})
}
// MARK: - Reminder Time Button
private var reminderTimeButton: some View {
ZStack {
theme.currentTheme.secondaryBGColor
Button(action: {
EventLogger.log(event: "tap_reminder_time")
showReminderTimePicker = true
}, label: {
HStack(spacing: 12) {
Image(systemName: "clock.fill")
.font(.title2)
.foregroundColor(.orange)
.frame(width: 32)
VStack(alignment: .leading, spacing: 2) {
Text("Reminder Time")
.foregroundColor(textColor)
Text(formattedReminderTime)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundStyle(.tertiary)
}
.padding()
})
}
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
}
private var formattedReminderTime: String {
let onboardingData = UserDefaultsStore.getOnboarding()
let formatter = DateFormatter()
formatter.timeStyle = .short
return formatter.string(from: onboardingData.date)
}
// MARK: - Section Headers
private var featuresSectionHeader: some View {
@@ -80,6 +131,7 @@ struct SettingsContentView: View {
}
.padding(.top, 20)
.padding(.horizontal, 4)
.siriShortcutTip()
}
private var settingsSectionHeader: some View {
@@ -91,6 +143,7 @@ struct SettingsContentView: View {
}
.padding(.top, 20)
.padding(.horizontal, 4)
.controlCenterTip()
}
private var legalSectionHeader: some View {
@@ -180,8 +233,15 @@ struct SettingsContentView: View {
set: { newValue in
if newValue {
Task {
let success = await healthService.requestAuthorization()
if !success {
// Request read permissions for health insights
let readSuccess = await healthService.requestAuthorization()
// Request write permissions for State of Mind sync
do {
try await HealthKitManager.shared.requestAuthorization()
} catch {
print("HealthKit write authorization failed: \(error)")
}
if !readSuccess {
EventLogger.log(event: "healthkit_enable_failed")
}
}
@@ -202,6 +262,7 @@ struct SettingsContentView: View {
}
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
.healthKitSyncTip()
}
// MARK: - Export Data Button
@@ -311,6 +372,65 @@ struct SettingsContentView: View {
}
}
// MARK: - Reminder Time Picker View
struct ReminderTimePickerView: View {
@Environment(\.dismiss) private var dismiss
@State private var selectedTime: Date
init() {
let onboardingData = UserDefaultsStore.getOnboarding()
_selectedTime = State(initialValue: onboardingData.date)
}
var body: some View {
NavigationStack {
VStack(spacing: 24) {
Text("When would you like to be reminded to log your mood?")
.font(.headline)
.multilineTextAlignment(.center)
.padding(.top, 20)
DatePicker(
"Reminder Time",
selection: $selectedTime,
displayedComponents: .hourAndMinute
)
.datePickerStyle(.wheel)
.labelsHidden()
Spacer()
}
.padding()
.navigationTitle("Reminder Time")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Save") {
saveReminderTime()
dismiss()
}
.fontWeight(.semibold)
}
}
}
}
private func saveReminderTime() {
let onboardingData = UserDefaultsStore.getOnboarding()
onboardingData.date = selectedTime
// This handles notification scheduling and Live Activity rescheduling
OnboardingDataDataManager.shared.updateOnboardingData(onboardingData: onboardingData)
EventLogger.log(event: "reminder_time_updated")
}
}
// MARK: - Legacy SettingsView (sheet presentation with close button)
struct SettingsView: View {
@Environment(\.dismiss) var dismiss
@@ -583,8 +703,15 @@ struct SettingsView: View {
set: { newValue in
if newValue {
Task {
let success = await healthService.requestAuthorization()
if !success {
// Request read permissions for health insights
let readSuccess = await healthService.requestAuthorization()
// Request write permissions for State of Mind sync
do {
try await HealthKitManager.shared.requestAuthorization()
} catch {
print("HealthKit write authorization failed: \(error)")
}
if !readSuccess {
EventLogger.log(event: "healthkit_enable_failed")
}
}