Fix memory leaks and add debug tools, remove ControlCenterTip
Memory optimization: - Add onDisappear cleanup for repeatForever animations in LockScreenView - Add onDisappear cleanup for animations in FeelsSubscriptionStoreView - Add onDisappear cleanup in AddMoodHeaderView and PaywallPreviewSettingsView Debug improvements: - Add test data and clear data buttons to Settings (debug builds only) TipKit changes: - Remove ControlCenterTip (unused) - Add TipKit-Tips.md documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -891,6 +891,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Add Test Data" : {
|
||||
"comment" : "A button label that adds test data to the app.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Add the Mood Vote widget to quickly log your mood without opening the app." : {
|
||||
"comment" : "A message encouraging users to add the Mood Vote widget to their home screen to log moods without using the main app.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -1829,6 +1833,10 @@
|
||||
"comment" : "A description of the benefits of the premium subscription.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Clear All Data" : {
|
||||
"comment" : "A button label that clears all data from the app.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Clear DB" : {
|
||||
"comment" : "A button label that clears the app's database.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -4200,6 +4208,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Delete all mood entries" : {
|
||||
"comment" : "A description of what the \"Clear All Data\" button does.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Delete Entry" : {
|
||||
"comment" : "An alert that appears when the user confirms they want to delete an entry.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -9038,6 +9050,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Populate with sample mood entries" : {
|
||||
"comment" : "A description of what the \"Add Test Data\" button does.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Predict your patterns.\nPrepare for any weather." : {
|
||||
"comment" : "A description of the premium feature, \"Your Emotional Forecast\", that appears below the title.",
|
||||
"isCommentAutoGenerated" : true
|
||||
|
||||
@@ -150,28 +150,6 @@ struct MoodStreakTip: Tip {
|
||||
static var currentStreak: Int = 0
|
||||
}
|
||||
|
||||
/// Tip for Control Center widget
|
||||
struct ControlCenterTip: Tip {
|
||||
var title: Text {
|
||||
Text("Quick Access from Control Center")
|
||||
}
|
||||
|
||||
var message: Text? {
|
||||
Text("Add Feels to Control Center for one-tap mood logging from anywhere.")
|
||||
}
|
||||
|
||||
var image: Image? {
|
||||
Image(systemName: "slider.horizontal.3")
|
||||
}
|
||||
|
||||
var rules: [Rule] {
|
||||
#Rule(Self.$daysUsingApp) { $0 >= 5 }
|
||||
}
|
||||
|
||||
@Parameter
|
||||
static var daysUsingApp: Int = 0
|
||||
}
|
||||
|
||||
// MARK: - Tips Manager
|
||||
|
||||
@MainActor
|
||||
@@ -203,7 +181,6 @@ class TipsManager {
|
||||
|
||||
func updateDaysUsingApp(_ days: Int) {
|
||||
WidgetVotingTip.daysUsingApp = days
|
||||
ControlCenterTip.daysUsingApp = days
|
||||
}
|
||||
|
||||
func updateStreak(_ streak: Int) {
|
||||
@@ -241,10 +218,6 @@ extension View {
|
||||
func moodStreakTip() -> some View {
|
||||
self.popoverTip(MoodStreakTip())
|
||||
}
|
||||
|
||||
func controlCenterTip() -> some View {
|
||||
self.popoverTip(ControlCenterTip())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Inline Tip View
|
||||
|
||||
@@ -382,6 +382,9 @@ struct OrbitVotingView: View {
|
||||
centerPulse = 1.1
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
centerPulse = 1.0
|
||||
}
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityLabel(String(localized: "Mood selection"))
|
||||
}
|
||||
@@ -589,6 +592,9 @@ struct NeonVotingView: View {
|
||||
pulsePhase = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
pulsePhase = false
|
||||
}
|
||||
}
|
||||
|
||||
private var neonGridBackground: some View {
|
||||
|
||||
@@ -151,6 +151,10 @@ struct CelestialMarketingContent: View {
|
||||
showContent = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
animateGradient = false
|
||||
animateOrbs = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,6 +225,10 @@ struct GardenMarketingContent: View {
|
||||
showContent = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
bloomPhase = false
|
||||
swayPhase = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,6 +307,11 @@ struct NeonMarketingContent: View {
|
||||
showContent = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
pulsePhase = false
|
||||
glowPhase = false
|
||||
scanlineOffset = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,6 +371,9 @@ struct MinimalMarketingContent: View {
|
||||
showContent = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
breathe = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,6 +431,9 @@ struct ZenMarketingContent: View {
|
||||
showContent = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
inkFlow = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,6 +567,9 @@ struct MixtapeMarketingContent: View {
|
||||
showContent = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
tapeRotation = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,6 +634,9 @@ struct HeartfeltMarketingContent: View {
|
||||
showContent = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
heartbeat = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -674,6 +699,9 @@ struct LuxeMarketingContent: View {
|
||||
showContent = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
shimmer = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -738,6 +766,9 @@ struct ForecastMarketingContent: View {
|
||||
showContent = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
cloudDrift = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,6 +833,9 @@ struct PlayfulMarketingContent: View {
|
||||
showContent = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
bounce = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -858,6 +892,9 @@ struct JournalMarketingContent: View {
|
||||
showContent = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
pageFlip = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1452,6 +1489,9 @@ struct EmotionOrb: View {
|
||||
pulse = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
pulse = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,22 +118,6 @@ struct AuroraBackground: View {
|
||||
.blur(radius: 40)
|
||||
.offset(y: animateGradient ? -10 : 10)
|
||||
|
||||
// Noise texture overlay
|
||||
Rectangle()
|
||||
.fill((isDark ? Color.white : Color.black).opacity(0.015))
|
||||
.background(
|
||||
Canvas { context, size in
|
||||
for _ in 0..<1000 {
|
||||
let x = CGFloat.random(in: 0...size.width)
|
||||
let y = CGFloat.random(in: 0...size.height)
|
||||
let opacity = Double.random(in: 0.01...0.04)
|
||||
context.fill(
|
||||
Path(ellipseIn: CGRect(x: x, y: y, width: 1, height: 1)),
|
||||
with: .color((isDark ? Color.white : Color.black).opacity(opacity))
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.onAppear {
|
||||
@@ -365,6 +349,9 @@ struct ZenLockBackground: View {
|
||||
breathe = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
breathe = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,6 +388,9 @@ struct ZenEnsoOrb: View {
|
||||
breathe = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
breathe = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,6 +440,9 @@ struct NeonLockBackground: View {
|
||||
pulse = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
pulse = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,6 +494,10 @@ struct NeonRingOrb: View {
|
||||
pulse = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
rotate = false
|
||||
pulse = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,6 +557,9 @@ struct CelestialLockBackground: View {
|
||||
twinkle = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
twinkle = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,6 +620,10 @@ struct CelestialOrbsElement: View {
|
||||
float = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
rotate = false
|
||||
float = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -704,21 +708,6 @@ struct MixtapeLockBackground: View {
|
||||
endPoint: shift ? .bottomTrailing : .bottomLeading
|
||||
)
|
||||
|
||||
// Noise texture
|
||||
Rectangle()
|
||||
.fill(.white.opacity(0.03))
|
||||
.background(
|
||||
Canvas { context, size in
|
||||
for _ in 0..<500 {
|
||||
let x = CGFloat.random(in: 0...size.width)
|
||||
let y = CGFloat.random(in: 0...size.height)
|
||||
context.fill(
|
||||
Path(ellipseIn: CGRect(x: x, y: y, width: 1, height: 1)),
|
||||
with: .color(.white.opacity(Double.random(in: 0.02...0.06)))
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.onAppear {
|
||||
@@ -726,6 +715,9 @@ struct MixtapeLockBackground: View {
|
||||
shift = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
shift = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,6 +767,9 @@ struct CassetteElement: View {
|
||||
spin = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
spin = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -818,6 +813,9 @@ struct BloomLockBackground: View {
|
||||
bloom = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
bloom = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -867,6 +865,9 @@ struct FlowerElement: View {
|
||||
bloom = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
bloom = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -904,6 +905,9 @@ struct HeartfeltLockBackground: View {
|
||||
pulse = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
pulse = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -940,6 +944,9 @@ struct HeartElement: View {
|
||||
beat = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
beat = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1000,6 +1007,9 @@ struct MinimalCircleElement: View {
|
||||
breathe = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
breathe = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1037,6 +1047,9 @@ struct LuxeLockBackground: View {
|
||||
shimmer = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
shimmer = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1078,6 +1091,10 @@ struct DiamondElement: View {
|
||||
shimmer = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
rotate = false
|
||||
shimmer = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1117,6 +1134,9 @@ struct ForecastLockBackground: View {
|
||||
drift = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
drift = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1169,6 +1189,9 @@ struct WeatherElement: View {
|
||||
shine = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
shine = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1235,6 +1258,10 @@ struct PlayfulEmojiElement: View {
|
||||
bounce = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
wiggle = false
|
||||
bounce = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1326,6 +1353,9 @@ struct JournalBookElement: View {
|
||||
open = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
open = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1863,6 +1893,9 @@ struct NeonUnlockButton: View {
|
||||
pulse = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
pulse = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -386,6 +386,9 @@ struct CelestialMiniPreview: View {
|
||||
animate = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
animate = false
|
||||
}
|
||||
}
|
||||
|
||||
private var orbColors: [Color] {
|
||||
@@ -445,6 +448,9 @@ struct GardenMiniPreview: View {
|
||||
bloom = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
bloom = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,6 +514,9 @@ struct NeonMiniPreview: View {
|
||||
pulse = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
pulse = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,6 +561,9 @@ struct MinimalMiniPreview: View {
|
||||
breathe = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
breathe = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,6 +604,9 @@ struct ZenMiniPreview: View {
|
||||
breathe = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
breathe = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,6 +683,9 @@ struct MixtapeMiniPreview: View {
|
||||
spin = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
spin = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -710,6 +728,9 @@ struct HeartfeltMiniPreview: View {
|
||||
beat = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
beat = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,6 +777,9 @@ struct LuxeMiniPreview: View {
|
||||
shimmer = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
shimmer = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -801,6 +825,9 @@ struct ForecastMiniPreview: View {
|
||||
drift = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
drift = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -847,6 +874,9 @@ struct PlayfulMiniPreview: View {
|
||||
bounce = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
bounce = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@ struct SettingsContentView: View {
|
||||
trialDateButton
|
||||
animationLabButton
|
||||
paywallPreviewButton
|
||||
addTestDataButton
|
||||
clearDataButton
|
||||
#endif
|
||||
|
||||
Spacer()
|
||||
@@ -159,7 +161,6 @@ struct SettingsContentView: View {
|
||||
}
|
||||
.padding(.top, 20)
|
||||
.padding(.horizontal, 4)
|
||||
.controlCenterTip()
|
||||
}
|
||||
|
||||
private var legalSectionHeader: some View {
|
||||
@@ -331,6 +332,66 @@ struct SettingsContentView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var addTestDataButton: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button {
|
||||
DataController.shared.populateTestData()
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "plus.square.on.square")
|
||||
.font(.title2)
|
||||
.foregroundColor(.green)
|
||||
.frame(width: 32)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Add Test Data")
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text("Populate with sample mood entries")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
private var clearDataButton: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button {
|
||||
DataController.shared.clearDB()
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "trash")
|
||||
.font(.title2)
|
||||
.foregroundColor(.red)
|
||||
.frame(width: 32)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Clear All Data")
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text("Delete all mood entries")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - Privacy Lock Toggle
|
||||
|
||||
@@ -126,7 +126,6 @@ This document covers the new Apple-specific features integrated into Feels, incl
|
||||
| WidgetVotingTip | Day view | After first mood log |
|
||||
| TimeViewTip | Day view header | After 2 days usage |
|
||||
| MoodStreakTip | Day view | When streak >= 3 |
|
||||
| ControlCenterTip | Settings | After 10 mood logs |
|
||||
|
||||
### How to Test
|
||||
1. Tips appear automatically based on conditions
|
||||
|
||||
152
docs/TipKit-Tips.md
Normal file
152
docs/TipKit-Tips.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# TipKit Tips Documentation
|
||||
|
||||
This document describes all TipKit tips implemented in the Feels app, including their display conditions and locations.
|
||||
|
||||
## Overview
|
||||
|
||||
Tips are managed by `TipsManager` (singleton) and configured with:
|
||||
- **Display Frequency**: Daily
|
||||
- **Datastore Location**: Application default
|
||||
|
||||
---
|
||||
|
||||
## Tips
|
||||
|
||||
### 1. CustomizeLayoutTip
|
||||
|
||||
**Title**: "Personalize Your Experience"
|
||||
**Message**: "Tap here to customize mood icons, colors, and layouts."
|
||||
**Icon**: `paintbrush`
|
||||
|
||||
**Display Conditions**: Always eligible (no rules)
|
||||
|
||||
**Location**: CustomizeContentView (top of the Customize tab in Settings)
|
||||
|
||||
---
|
||||
|
||||
### 2. AIInsightsTip
|
||||
|
||||
**Title**: "Discover AI Insights"
|
||||
**Message**: "Get personalized insights about your mood patterns powered by Apple Intelligence."
|
||||
**Icon**: `brain`
|
||||
|
||||
**Display Conditions**:
|
||||
- User has logged at least **7 moods**
|
||||
|
||||
**Parameter**: `hasLoggedMoods: Int` (incremented via `TipsManager.shared.onMoodLogged()`)
|
||||
|
||||
**Location**: InsightsView
|
||||
|
||||
---
|
||||
|
||||
### 3. SiriShortcutTip
|
||||
|
||||
**Title**: "Use Siri to Log Moods"
|
||||
**Message**: "Say 'Hey Siri, log my mood as great in Feels' for hands-free logging."
|
||||
**Icon**: `mic.fill`
|
||||
|
||||
**Display Conditions**:
|
||||
- User has logged at least **3 moods**
|
||||
|
||||
**Parameter**: `moodLogCount: Int` (incremented via `TipsManager.shared.onMoodLogged()`)
|
||||
|
||||
**Location**: SettingsContentView (Features section header, via `.siriShortcutTip()`)
|
||||
|
||||
---
|
||||
|
||||
### 4. HealthKitSyncTip
|
||||
|
||||
**Title**: "Sync with Apple Health"
|
||||
**Message**: "Connect to Apple Health to see your mood data alongside sleep, exercise, and more."
|
||||
**Icon**: `heart.fill`
|
||||
|
||||
**Display Conditions**:
|
||||
- User has viewed the Settings screen
|
||||
|
||||
**Parameter**: `hasSeenSettings: Bool` (set via `TipsManager.shared.onSettingsViewed()`)
|
||||
|
||||
**Location**: SettingsContentView (Health Kit toggle, via `.healthKitSyncTip()`)
|
||||
|
||||
---
|
||||
|
||||
### 5. WidgetVotingTip
|
||||
|
||||
**Title**: "Vote from Your Home Screen"
|
||||
**Message**: "Add the Mood Vote widget to quickly log your mood without opening the app."
|
||||
**Icon**: `square.grid.2x2`
|
||||
|
||||
**Display Conditions**:
|
||||
- User has been using the app for at least **2 days**
|
||||
|
||||
**Parameter**: `daysUsingApp: Int` (updated via `TipsManager.shared.updateDaysUsingApp(_:)`)
|
||||
|
||||
**Location**: DayView
|
||||
|
||||
---
|
||||
|
||||
### 6. TimeViewTip
|
||||
|
||||
**Title**: "View Your History"
|
||||
**Message**: "Switch between Day, Month, and Year views to see your mood patterns over time."
|
||||
**Icon**: `calendar`
|
||||
|
||||
**Display Conditions**: Always eligible (no rules)
|
||||
|
||||
**Location**: DayView
|
||||
|
||||
---
|
||||
|
||||
### 7. MoodStreakTip
|
||||
|
||||
**Title**: "Build Your Streak!"
|
||||
**Message**: "Log your mood daily to build a streak. Consistency helps you understand your patterns."
|
||||
**Icon**: `flame.fill`
|
||||
|
||||
**Display Conditions**:
|
||||
- User has a current streak of at least **3 days**
|
||||
|
||||
**Parameter**: `currentStreak: Int` (updated via `TipsManager.shared.updateStreak(_:)`)
|
||||
|
||||
**Location**: DayView
|
||||
|
||||
---
|
||||
|
||||
## TipsManager API
|
||||
|
||||
```swift
|
||||
// Configure tips (call on app launch)
|
||||
TipsManager.shared.configure()
|
||||
|
||||
// Reset all tips (for testing)
|
||||
TipsManager.shared.resetAllTips()
|
||||
|
||||
// Update parameters
|
||||
TipsManager.shared.onMoodLogged() // Increments mood log count
|
||||
TipsManager.shared.onSettingsViewed() // Marks settings as viewed
|
||||
TipsManager.shared.updateDaysUsingApp(_:) // Updates days using app
|
||||
TipsManager.shared.updateStreak(_:) // Updates current streak
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## View Modifiers
|
||||
|
||||
Tips can be attached to views using these convenience modifiers:
|
||||
|
||||
```swift
|
||||
.customizeLayoutTip()
|
||||
.aiInsightsTip()
|
||||
.siriShortcutTip()
|
||||
.healthKitSyncTip()
|
||||
.widgetVotingTip()
|
||||
.timeViewTip()
|
||||
.moodStreakTip()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files
|
||||
|
||||
- **Definition**: `Shared/FeelsTips.swift`
|
||||
- **Manager**: `TipsManager` class in same file
|
||||
- **Configuration**: Called in `FeelsApp.swift`
|
||||
Reference in New Issue
Block a user