diff --git a/Feels Watch App/ContentView.swift b/Feels Watch App/ContentView.swift index 761b4e3..6c52258 100644 --- a/Feels Watch App/ContentView.swift +++ b/Feels Watch App/ContentView.swift @@ -121,6 +121,10 @@ enum WatchMoodImageStyle: Int { case fontAwesome = 0 case emoji = 1 case handEmoji = 2 + case weather = 3 + case garden = 4 + case hearts = 5 + case cosmic = 6 static var current: WatchMoodImageStyle { // Use optional chaining for preview safety - App Group may not exist in canvas @@ -161,6 +165,46 @@ enum WatchMoodImageStyle: Int { case .horrible: return "πŸ–•" case .missing, .placeholder: return "❓" } + case .weather: + switch mood { + case .great: return "β˜€οΈ" + case .good: return "β›…" + case .average: return "☁️" + case .bad: return "🌧️" + case .horrible: return "β›ˆοΈ" + case .missing: return "🌫️" + case .placeholder: return "❓" + } + case .garden: + switch mood { + case .great: return "🌸" + case .good: return "🌿" + case .average: return "🌱" + case .bad: return "πŸ‚" + case .horrible: return "πŸ₯€" + case .missing: return "πŸ•³οΈ" + case .placeholder: return "❓" + } + case .hearts: + switch mood { + case .great: return "πŸ’–" + case .good: return "🩷" + case .average: return "🀍" + case .bad: return "🩢" + case .horrible: return "πŸ’”" + case .missing: return "πŸ–€" + case .placeholder: return "❓" + } + case .cosmic: + switch mood { + case .great: return "⭐" + case .good: return "πŸŒ•" + case .average: return "πŸŒ“" + case .bad: return "πŸŒ‘" + case .horrible: return "πŸ•³οΈ" + case .missing: return "✧" + case .placeholder: return "❓" + } } } } diff --git a/FeelsWidget2/FeelsVoteWidget.swift b/FeelsWidget2/FeelsVoteWidget.swift index e1a9f15..ed22028 100644 --- a/FeelsWidget2/FeelsVoteWidget.swift +++ b/FeelsWidget2/FeelsVoteWidget.swift @@ -222,7 +222,6 @@ struct FeelsVoteWidgetEntryView: View { struct VotingView: View { let family: WidgetFamily let promptText: String - let moods: [Mood] = [.horrible, .bad, .average, .good, .great] private var moodTint: MoodTintable.Type { UserDefaultsStore.moodTintable() @@ -233,34 +232,79 @@ struct VotingView: View { } var body: some View { - VStack(spacing: 12) { + if family == .systemSmall { + smallLayout + } else { + mediumLayout + } + } + + // MARK: - Small Widget: 3 over 2 grid + private var smallLayout: some View { + VStack(spacing: 0) { + Text(promptText) + .font(.caption) + .foregroundStyle(.primary) + .multilineTextAlignment(.center) + .lineLimit(1) + .minimumScaleFactor(0.7) + .padding(.bottom, 10) + + // Top row: Great, Good, Average + HStack(spacing: 12) { + ForEach([Mood.great, .good, .average], id: \.rawValue) { mood in + moodButton(for: mood, size: 36) + } + } + .padding(.bottom, 6) + + // Bottom row: Bad, Horrible + HStack(spacing: 12) { + ForEach([Mood.bad, .horrible], id: \.rawValue) { mood in + moodButton(for: mood, size: 36) + } + } + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + } + + // MARK: - Medium Widget: Single row + private var mediumLayout: some View { + VStack { Text(promptText) .font(.headline) .foregroundStyle(.primary) .multilineTextAlignment(.center) .lineLimit(2) .minimumScaleFactor(0.8) + .padding(.bottom, 20) - HStack(spacing: 8) { - ForEach(moods, id: \.rawValue) { mood in - Button(intent: VoteMoodIntent(mood: mood)) { - moodImages.icon(forMood: mood) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: family == .systemSmall ? 36 : 44, height: family == .systemSmall ? 36 : 44) - .foregroundColor(moodTint.color(forMood: mood)) - } - .buttonStyle(.plain) + HStack(spacing: 16) { + ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in + moodButton(for: mood, size: 44) } } } .padding() } + + private func moodButton(for mood: Mood, size: CGFloat) -> some View { + Button(intent: VoteMoodIntent(mood: mood)) { + moodImages.icon(forMood: mood) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: size, height: size) + .foregroundColor(moodTint.color(forMood: mood)) + } + .buttonStyle(.plain) + } } // MARK: - Voted Stats View (shown after voting) struct VotedStatsView: View { + @Environment(\.widgetFamily) var family let entry: VoteWidgetEntry private var moodTint: MoodTintable.Type { @@ -272,51 +316,110 @@ struct VotedStatsView: View { } var body: some View { - VStack(spacing: 12) { - // Today's mood + if family == .systemSmall { + smallLayout + } else { + mediumLayout + } + } + + // MARK: - Small: Centered mood with checkmark + private var smallLayout: some View { + VStack(spacing: 8) { if let mood = entry.todaysMood { - HStack(spacing: 8) { + // Large centered mood icon + ZStack(alignment: .bottomTrailing) { moodImages.icon(forMood: mood) .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 32, height: 32) + .frame(width: 56, height: 56) .foregroundColor(moodTint.color(forMood: mood)) - VStack(alignment: .leading, spacing: 2) { - Text("Today") - .font(.caption) - .foregroundStyle(.secondary) - Text(mood.widgetDisplayName) - .font(.headline) - .foregroundColor(moodTint.color(forMood: mood)) - } + // Checkmark badge + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 18)) + .foregroundColor(.green) + .background(Circle().fill(.white).frame(width: 14, height: 14)) + .offset(x: 4, y: 4) + } - Spacer() + Text("Logged!") + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + + if let stats = entry.stats { + Text("\(stats.totalEntries) day streak") + .font(.caption2) + .foregroundStyle(.tertiary) } } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(12) + } - // Stats - if let stats = entry.stats { - Divider() + // MARK: - Medium: Mood + stats bar + private var mediumLayout: some View { + HStack(spacing: 20) { + if let mood = entry.todaysMood { + // Left: Mood display + VStack(spacing: 6) { + moodImages.icon(forMood: mood) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 48, height: 48) + .foregroundColor(moodTint.color(forMood: mood)) - VStack(spacing: 4) { - Text("\(stats.totalEntries) entries") - .font(.caption) + Text(mood.widgetDisplayName) + .font(.subheadline.weight(.semibold)) + .foregroundColor(moodTint.color(forMood: mood)) + + Text("Today") + .font(.caption2) .foregroundStyle(.secondary) + } - GeometryReader { geo in - HStack(spacing: 2) { + // Right: Stats + if let stats = entry.stats { + VStack(alignment: .leading, spacing: 8) { + Text("\(stats.totalEntries) entries") + .font(.caption.weight(.medium)) + .foregroundStyle(.primary) + + // Mini mood breakdown + HStack(spacing: 6) { ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in - let percentage = stats.percentage(for: mood) - if percentage > 0 { - RoundedRectangle(cornerRadius: 2) - .fill(moodTint.color(forMood: mood)) - .frame(width: max(4, geo.size.width * CGFloat(percentage) / 100)) + let count = stats.moodCounts[mood, default: 0] + if count > 0 { + HStack(spacing: 2) { + Circle() + .fill(moodTint.color(forMood: mood)) + .frame(width: 8, height: 8) + Text("\(count)") + .font(.caption2) + .foregroundStyle(.secondary) + } } } } + + // Progress bar + GeometryReader { geo in + HStack(spacing: 1) { + ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in + let percentage = stats.percentage(for: mood) + if percentage > 0 { + RoundedRectangle(cornerRadius: 2) + .fill(moodTint.color(forMood: mood)) + .frame(width: max(4, geo.size.width * CGFloat(percentage) / 100)) + } + } + } + } + .frame(height: 8) + .clipShape(RoundedRectangle(cornerRadius: 4)) } - .frame(height: 12) + .frame(maxWidth: .infinity, alignment: .leading) } } } @@ -335,15 +438,15 @@ struct NonSubscriberView: View { .foregroundStyle(.pink) Text("Track Your Mood") - .font(.headline) + .font(.subheadline.weight(.semibold)) .foregroundStyle(.primary) + .minimumScaleFactor(0.8) Text("Tap to subscribe") - .font(.caption) + .font(.caption2) .foregroundStyle(.secondary) - .multilineTextAlignment(.center) } - .padding() + .padding(12) .frame(maxWidth: .infinity, maxHeight: .infinity) } } diff --git a/FeelsWidget2/FeelsWidget.swift b/FeelsWidget2/FeelsWidget.swift index c98495a..1755331 100644 --- a/FeelsWidget2/FeelsWidget.swift +++ b/FeelsWidget2/FeelsWidget.swift @@ -726,52 +726,117 @@ struct FeelsGraphicWidget: Widget { } } +// MARK: - Preview Helpers + +private extension FeelsWidget_Previews { + static func sampleTimelineViews(count: Int) -> [WatchTimelineView] { + let moods: [Mood] = [.great, .good, .average, .bad, .horrible] + return (0.. SimpleEntry { + SimpleEntry( + date: Date(), + configuration: ConfigurationIntent(), + timeLineViews: sampleTimelineViews(count: timelineCount), + hasSubscription: true, + hasVotedToday: true + ) + } +} + struct FeelsWidget_Previews: PreviewProvider { static var previews: some View { Group { - FeelsGraphicWidgetEntryView(entry: SimpleEntry(date: Date(), - configuration: ConfigurationIntent(), - timeLineViews: [WatchTimelineView(image: HandEmojiMoodImages.icon(forMood: .great), - graphic: HandEmojiMoodImages.icon(forMood: .great), - date: Date(), - color: MoodTints.Neon.color(forMood: .great), - - secondaryColor: .white), - WatchTimelineView(image: HandEmojiMoodImages.icon(forMood: .great), - graphic: HandEmojiMoodImages.icon(forMood: .great), - date: Date(), - color: MoodTints.Neon.color(forMood: .great), - - secondaryColor: .white)])) + // MARK: - FeelsWidget (Timeline) + FeelsWidgetEntryView(entry: sampleEntry(timelineCount: 1)) .previewContext(WidgetPreviewContext(family: .systemSmall)) - - FeelsGraphicWidgetEntryView(entry: SimpleEntry(date: Date(), - configuration: ConfigurationIntent(), - timeLineViews: [WatchTimelineView(image: HandEmojiMoodImages.icon(forMood: .horrible), - graphic: HandEmojiMoodImages.icon(forMood: .horrible), - date: Date(), - color: MoodTints.Neon.color(forMood: .horrible), - - secondaryColor: .white), - WatchTimelineView(image: HandEmojiMoodImages.icon(forMood: .horrible), - graphic: HandEmojiMoodImages.icon(forMood: .horrible), - date: Date(), - color: MoodTints.Neon.color(forMood: .horrible), - - secondaryColor: .white)])) + .previewDisplayName("Timeline - Small") + + FeelsWidgetEntryView(entry: sampleEntry(timelineCount: 5)) + .previewContext(WidgetPreviewContext(family: .systemMedium)) + .previewDisplayName("Timeline - Medium") + + FeelsWidgetEntryView(entry: sampleEntry(timelineCount: 10)) + .previewContext(WidgetPreviewContext(family: .systemLarge)) + .previewDisplayName("Timeline - Large") + + // MARK: - FeelsGraphicWidget (Mood Graphic) + FeelsGraphicWidgetEntryView(entry: sampleEntry(timelineCount: 2)) .previewContext(WidgetPreviewContext(family: .systemSmall)) - - // FeelsWidgetEntryView(entry: SimpleEntry(date: Date(), - // configuration: ConfigurationIntent(), - // timeLineViews: FeelsWidget_Previews.data)) - // .previewContext(WidgetPreviewContext(family: .systemMedium)) - // .environment(\.sizeCategory, .medium) - // - // FeelsWidgetEntryView(entry: SimpleEntry(date: Date(), - // configuration: ConfigurationIntent(), - // timeLineViews: FeelsWidget_Previews.data)) - // .previewContext(WidgetPreviewContext(family: .systemLarge)) - // .environment(\.sizeCategory, .large) + .previewDisplayName("Mood Graphic - Small") + + // MARK: - FeelsIconWidget (Custom Icon) + FeelsIconWidgetEntryView(entry: sampleEntry()) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + .previewDisplayName("Custom Icon - Small") + + // MARK: - FeelsVoteWidget (Vote - Not Voted) + FeelsVoteWidgetEntryView(entry: VoteWidgetEntry( + date: Date(), + hasSubscription: true, + hasVotedToday: false, + todaysMood: nil, + stats: nil, + promptText: "How are you feeling?" + )) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + .previewDisplayName("Vote - Small (Not Voted)") + + FeelsVoteWidgetEntryView(entry: VoteWidgetEntry( + date: Date(), + hasSubscription: true, + hasVotedToday: false, + todaysMood: nil, + stats: nil, + promptText: "How are you feeling?" + )) + .previewContext(WidgetPreviewContext(family: .systemMedium)) + .previewDisplayName("Vote - Medium (Not Voted)") + + // MARK: - FeelsVoteWidget (Vote - Already Voted) + FeelsVoteWidgetEntryView(entry: VoteWidgetEntry( + date: Date(), + hasSubscription: true, + hasVotedToday: true, + todaysMood: .great, + stats: MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1]), + promptText: "" + )) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + .previewDisplayName("Vote - Small (Voted)") + + FeelsVoteWidgetEntryView(entry: VoteWidgetEntry( + date: Date(), + hasSubscription: true, + hasVotedToday: true, + todaysMood: .good, + stats: MoodStats(totalEntries: 45, moodCounts: [.great: 15, .good: 18, .average: 8, .bad: 3, .horrible: 1]), + promptText: "" + )) + .previewContext(WidgetPreviewContext(family: .systemMedium)) + .previewDisplayName("Vote - Medium (Voted)") + + // MARK: - FeelsVoteWidget (Non-Subscriber) + FeelsVoteWidgetEntryView(entry: VoteWidgetEntry( + date: Date(), + hasSubscription: false, + hasVotedToday: false, + todaysMood: nil, + stats: nil, + promptText: "" + )) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + .previewDisplayName("Vote - Small (Non-Subscriber)") } } } diff --git a/Shared/Models/MoodImagable.swift b/Shared/Models/MoodImagable.swift index 398de35..f2eb235 100644 --- a/Shared/Models/MoodImagable.swift +++ b/Shared/Models/MoodImagable.swift @@ -15,19 +15,30 @@ enum MoodImages: Int, CaseIterable { case FontAwesome case Emoji case HandEmjoi - + case Weather + case Garden + case Hearts + case Cosmic + func icon(forMood mood: Mood) -> Image { switch self { - case .FontAwesome: return FontAwesomeMoodImages.icon(forMood: mood) case .Emoji: return EmojiMoodImages.icon(forMood: mood) case .HandEmjoi: return HandEmojiMoodImages.icon(forMood: mood) + case .Weather: + return WeatherMoodImages.icon(forMood: mood) + case .Garden: + return GardenMoodImages.icon(forMood: mood) + case .Hearts: + return HeartsMoodImages.icon(forMood: mood) + case .Cosmic: + return CosmicMoodImages.icon(forMood: mood) } } - + var moodImages: MoodImagable.Type { switch self { case .FontAwesome: @@ -36,6 +47,14 @@ enum MoodImages: Int, CaseIterable { return EmojiMoodImages.self case .HandEmjoi: return HandEmojiMoodImages.self + case .Weather: + return WeatherMoodImages.self + case .Garden: + return GardenMoodImages.self + case .Hearts: + return HeartsMoodImages.self + case .Cosmic: + return CosmicMoodImages.self } } } @@ -102,3 +121,87 @@ final class HandEmojiMoodImages: MoodImagable { } } } + +final class WeatherMoodImages: MoodImagable { + static func icon(forMood mood: Mood) -> Image { + switch mood { + case .horrible: + return Image(uiImage: "β›ˆοΈ".textToImage()!) + case .bad: + return Image(uiImage: "🌧️".textToImage()!) + case .average: + return Image(uiImage: "☁️".textToImage()!) + case .good: + return Image(uiImage: "β›…".textToImage()!) + case .great: + return Image(uiImage: "β˜€οΈ".textToImage()!) + case .missing: + return Image(uiImage: "🌫️".textToImage()!) + case .placeholder: + return Image("xmark-solid", bundle: .main) + } + } +} + +final class GardenMoodImages: MoodImagable { + static func icon(forMood mood: Mood) -> Image { + switch mood { + case .horrible: + return Image(uiImage: "πŸ₯€".textToImage()!) + case .bad: + return Image(uiImage: "πŸ‚".textToImage()!) + case .average: + return Image(uiImage: "🌱".textToImage()!) + case .good: + return Image(uiImage: "🌿".textToImage()!) + case .great: + return Image(uiImage: "🌸".textToImage()!) + case .missing: + return Image(uiImage: "πŸ•³οΈ".textToImage()!) + case .placeholder: + return Image("xmark-solid", bundle: .main) + } + } +} + +final class HeartsMoodImages: MoodImagable { + static func icon(forMood mood: Mood) -> Image { + switch mood { + case .horrible: + return Image(uiImage: "πŸ’”".textToImage()!) + case .bad: + return Image(uiImage: "🩢".textToImage()!) + case .average: + return Image(uiImage: "🀍".textToImage()!) + case .good: + return Image(uiImage: "🩷".textToImage()!) + case .great: + return Image(uiImage: "πŸ’–".textToImage()!) + case .missing: + return Image(uiImage: "πŸ–€".textToImage()!) + case .placeholder: + return Image("xmark-solid", bundle: .main) + } + } +} + +final class CosmicMoodImages: MoodImagable { + static func icon(forMood mood: Mood) -> Image { + switch mood { + case .horrible: + return Image(uiImage: "πŸ•³οΈ".textToImage()!) + case .bad: + return Image(uiImage: "πŸŒ‘".textToImage()!) + case .average: + return Image(uiImage: "πŸŒ“".textToImage()!) + case .good: + return Image(uiImage: "πŸŒ•".textToImage()!) + case .great: + return Image(uiImage: "⭐".textToImage()!) + case .missing: + return Image(uiImage: "✧".textToImage()!) + case .placeholder: + return Image("xmark-solid", bundle: .main) + } + } +} diff --git a/Shared/Views/AddMoodHeaderView.swift b/Shared/Views/AddMoodHeaderView.swift index 8dfa34f..7d2be7d 100644 --- a/Shared/Views/AddMoodHeaderView.swift +++ b/Shared/Views/AddMoodHeaderView.swift @@ -11,6 +11,7 @@ import SwiftData struct AddMoodHeaderView: View { @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system + @AppStorage(UserDefaultsStore.Keys.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var imagePack: MoodImages = .FontAwesome @AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor @AppStorage(UserDefaultsStore.Keys.votingLayoutStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var votingLayoutStyle: Int = 0 @@ -29,6 +30,10 @@ struct AddMoodHeaderView: View { var body: some View { ZStack { + // Force re-render when image pack changes + Text(String(imagePack.rawValue)) + .hidden() + theme.currentTheme.secondaryBGColor VStack(spacing: 16) { diff --git a/docs/mood-icon-concepts.html b/docs/mood-icon-concepts.html new file mode 100644 index 0000000..617de21 --- /dev/null +++ b/docs/mood-icon-concepts.html @@ -0,0 +1,1028 @@ + + + + + + Feels β€” Mood Icon System Concepts + + + + + + +
+
+
Design Reference Document
+

Mood Icon Systems

+

Exploration of 10 thematic icon systems for the Feels mood tracking app. Each concept provides visual metaphors for the emotional spectrum from horrible to great.

+ +
+
+
+ Horrible +
+
+
+ Bad +
+
+
+ Average +
+
+
+ Good +
+
+
+ Great +
+
+
+ Missing +
+
+
+ Placeholder +
+
+
+ +
+ + +
+
+
+
01
+

Weather

+

Atmospheric conditions as emotional states. Universal metaphor that translates across cultures β€” everyone understands a sunny day vs. a storm.

+
+
Best For
+
General audiences, wellness apps, users who prefer indirect emotional expression
+
+
+
+
+
+ β›ˆοΈ + Horrible +
+
+ 🌧️ + Bad +
+
+ ☁️ + Average +
+
+ β›… + Good +
+
+ β˜€οΈ + Great +
+
+ 🌫️ + Missing +
+
+ β—Œ + Placeholder +
+
+
+
+ +
+ + +
+
+
+
02
+

Garden

+

Plant life cycle from wilted to flourishing. Organic, nurturing metaphor that frames emotional health as something you cultivate over time.

+
+
Best For
+
Mindfulness-focused users, nature lovers, those who view wellness as growth
+
+
+
+
+
+ πŸ₯€ + Horrible +
+
+ πŸ‚ + Bad +
+
+ 🌱 + Average +
+
+ 🌿 + Good +
+
+ 🌸 + Great +
+
+ πŸ•³οΈ + Missing +
+
+ ⚬ + Placeholder +
+
+
+
+ +
+ + +
+
+
+
03
+

Hearts

+

Heart states from broken to overflowing. Direct emotional symbolism that feels personal and intimate. The universal symbol of feeling.

+
+
Best For
+
Emotionally expressive users, relationship-focused tracking, romantic aesthetic preference
+
+
+
+
+
+ πŸ’” + Horrible +
+
+ 🩢 + Bad +
+
+ 🀍 + Average +
+
+ 🩷 + Good +
+
+ πŸ’– + Great +
+
+ πŸ–€ + Missing +
+
+ β™‘ + Placeholder +
+
+
+
+ +
+ + +
+
+
+
04
+

Energy

+

Fire and energy levels from extinguished to blazing. Dynamic, active metaphor that frames mood as vitality and inner power.

+
+
Best For
+
Active/athletic users, those tracking energy alongside mood, motivational framing
+
+
+
+
+
+ πŸͺ¨ + Horrible +
+
+ πŸ’¨ + Bad +
+
+ πŸ•―οΈ + Average +
+
+ πŸ”₯ + Good +
+
+ ⚑ + Great +
+
+ ⬛ + Missing +
+
+ β—‡ + Placeholder +
+
+
+
+ +
+ + +
+
+
+
05
+

Ocean

+

Water states from turbulent depths to calm, sparkling surface. Evokes the ebb and flow of emotions with aquatic serenity.

+
+
Best For
+
Meditation practitioners, beach/ocean lovers, calm aesthetic preference
+
+
+
+
+
+ 🌊 + Horrible +
+
+ πŸŒ€ + Bad +
+
+ πŸ’§ + Average +
+
+ 🐚 + Good +
+
+ 🐬 + Great +
+
+ 🫧 + Missing +
+
+ β—‹ + Placeholder +
+
+
+
+ +
+ + +
+
+
+
06
+

Cosmic

+

Celestial objects from black holes to radiant stars. Vast, awe-inspiring metaphor that puts daily emotions in a grander perspective.

+
+
Best For
+
Astronomy enthusiasts, philosophical users, dark mode lovers, night owls
+
+
+
+
+
+ πŸ•³οΈ + Horrible +
+
+ πŸŒ‘ + Bad +
+
+ πŸŒ“ + Average +
+
+ πŸŒ• + Good +
+
+ ⭐ + Great +
+
+ ✧ + Missing +
+
+ Β· + Placeholder +
+
+
+
+ +
+ + +
+
+
+
07
+

Spirit Animals

+

Animal archetypes representing different emotional states. Playful approach that externalizes feelings as creature companions.

+
+
Best For
+
Playful users, animal lovers, those who prefer indirect expression, younger demographics
+
+
+
+
+
+ πŸ¦” + Horrible +
+
+ 🐌 + Bad +
+
+ 🐒 + Average +
+
+ πŸ¦‹ + Good +
+
+ πŸ¦… + Great +
+
+ 🐾 + Missing +
+
+ πŸ₯š + Placeholder +
+
+
+
+ +
+ + +
+
+
+
08
+

Beverages

+

Drink states from bitter to sweet celebration. Relatable daily ritual metaphor β€” mood as the drink you need or deserve right now.

+
+
Best For
+
Coffee/tea lovers, foodies, casual/humorous tone preference
+
+
+
+
+
+ 🧊 + Horrible +
+
+ πŸ«– + Bad +
+
+ β˜• + Average +
+
+ πŸ§‹ + Good +
+
+ 🍾 + Great +
+
+ πŸ«— + Missing +
+
+ πŸ₯› + Placeholder +
+
+
+
+ +
+ + +
+
+
+
09
+

Soundtrack

+

Musical expressions from silence to symphony. Mood as the music playing in your mind β€” what genre is your day?

+
+
Best For
+
Music lovers, creative/artistic users, those who connect emotion to sound
+
+
+
+
+
+ πŸ”‡ + Horrible +
+
+ πŸ”ˆ + Bad +
+
+ 🎡 + Average +
+
+ 🎢 + Good +
+
+ 🎸 + Great +
+
+ ⏸️ + Missing +
+
+ ⏺️ + Placeholder +
+
+
+
+ +
+ + +
+
+
+
10
+

Abstract

+

Geometric shapes representing emotional complexity. Minimal, non-figurative approach for those who prefer understated, design-forward aesthetics.

+
+
Best For
+
Minimalists, design-conscious users, those who want subtle/private expression
+
+
+
+
+
+ β–Ό + Horrible +
+
+ β—† + Bad +
+
+ β–  + Average +
+
+ ● + Good +
+
+ β˜… + Great +
+
+ β—‹ + Missing +
+
+ Β· + Placeholder +
+
+
+
+ +
+ +
+ + +
+

Implementation Notes

+
+
+

Technical Considerations

+
    +
  • All emoji options render natively on iOS without custom assets
  • +
  • SF Symbols alternatives provided for each concept (requires custom drawing)
  • +
  • Consider emoji skin tone variations where applicable
  • +
  • Test emoji rendering across iOS versions (some are newer)
  • +
  • Placeholder/missing states should be visually distinct but unobtrusive
  • +
+
+
+

User Preference System

+
    +
  • Allow users to select their preferred icon style in Settings
  • +
  • Store preference in UserDefaults (existing pattern)
  • +
  • Consider offering 2-3 options to avoid choice paralysis
  • +
  • Default to "Weather" or "Hearts" as most universally understood
  • +
  • Could offer as premium/subscription unlock
  • +
+
+
+

Accessibility

+
    +
  • Ensure VoiceOver reads mood names, not emoji descriptions
  • +
  • Color palettes should maintain sufficient contrast
  • +
  • Consider adding subtle labels in larger widget sizes
  • +
  • Test with reduced motion settings
  • +
+
+
+

Recommended Shortlist

+
    +
  • Weather β€” Most universal, works for everyone
  • +
  • Hearts β€” Emotional, personal, familiar
  • +
  • Garden β€” Wellness-aligned, growth mindset
  • +
  • Cosmic β€” Unique, works great in dark mode
  • +
+
+
+
+ +
+ +