Add Insights tab with 60+ randomized analytics
New Insights tab between Year and Customize with: - 20 insight generators producing 60+ unique insights - 5 random insights selected per section (month, year, all-time) - Categories: dominant mood, streaks, trends, positivity score, weekend vs weekday, mood swings, milestones, patterns, and more - Collapsible sections with mood-colored cards - Subscription paywall support - English and Spanish localization 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,8 +34,9 @@ struct FeelsApp: App {
|
|||||||
// build these here so when tints and other things get updated the views / their data dont
|
// build these here so when tints and other things get updated the views / their data dont
|
||||||
// have to get redrawn#imageLiteral(resourceName: "simulator_screenshot_0017B4DC-100B-42A3-A406-9019704AE275.png")
|
// have to get redrawn#imageLiteral(resourceName: "simulator_screenshot_0017B4DC-100B-42A3-A406-9019704AE275.png")
|
||||||
MainTabView(dayView: DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false)),
|
MainTabView(dayView: DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false)),
|
||||||
monthView: MonthView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: true)),
|
monthView: MonthView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: true)),
|
||||||
yearView: YearView(viewModel: YearViewModel()),
|
yearView: YearView(viewModel: YearViewModel()),
|
||||||
|
insightsView: InsightsView(),
|
||||||
customizeView: CustomizeView())
|
customizeView: CustomizeView())
|
||||||
.environment(\.managedObjectContext, persistenceController.viewContext)
|
.environment(\.managedObjectContext, persistenceController.viewContext)
|
||||||
.environmentObject(iapManager)
|
.environmentObject(iapManager)
|
||||||
|
|||||||
232
Shared/Views/InsightsView/InsightsView.swift
Normal file
232
Shared/Views/InsightsView/InsightsView.swift
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
//
|
||||||
|
// InsightsView.swift
|
||||||
|
// Feels
|
||||||
|
//
|
||||||
|
// Created by Claude Code on 12/9/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct InsightsView: View {
|
||||||
|
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||||
|
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
||||||
|
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||||
|
@AppStorage(UserDefaultsStore.Keys.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var imagePack: MoodImages = .FontAwesome
|
||||||
|
|
||||||
|
@StateObject private var viewModel = InsightsViewModel()
|
||||||
|
@EnvironmentObject var iapManager: IAPManager
|
||||||
|
@State private var showSubscriptionStore = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
ScrollView {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
// Header
|
||||||
|
HStack {
|
||||||
|
Text("Insights")
|
||||||
|
.font(.largeTitle.bold())
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
// This Month Section
|
||||||
|
InsightsSectionView(
|
||||||
|
title: "This Month",
|
||||||
|
icon: "calendar",
|
||||||
|
insights: viewModel.monthInsights,
|
||||||
|
textColor: textColor,
|
||||||
|
moodTint: moodTint,
|
||||||
|
imagePack: imagePack,
|
||||||
|
theme: theme
|
||||||
|
)
|
||||||
|
|
||||||
|
// This Year Section
|
||||||
|
InsightsSectionView(
|
||||||
|
title: "This Year",
|
||||||
|
icon: "calendar.badge.clock",
|
||||||
|
insights: viewModel.yearInsights,
|
||||||
|
textColor: textColor,
|
||||||
|
moodTint: moodTint,
|
||||||
|
imagePack: imagePack,
|
||||||
|
theme: theme
|
||||||
|
)
|
||||||
|
|
||||||
|
// All Time Section
|
||||||
|
InsightsSectionView(
|
||||||
|
title: "All Time",
|
||||||
|
icon: "infinity",
|
||||||
|
insights: viewModel.allTimeInsights,
|
||||||
|
textColor: textColor,
|
||||||
|
moodTint: moodTint,
|
||||||
|
imagePack: imagePack,
|
||||||
|
theme: theme
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding(.vertical)
|
||||||
|
.padding(.bottom, 100)
|
||||||
|
}
|
||||||
|
.disabled(iapManager.shouldShowPaywall)
|
||||||
|
|
||||||
|
if iapManager.shouldShowPaywall {
|
||||||
|
Color.black.opacity(0.3)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.onTapGesture {
|
||||||
|
showSubscriptionStore = true
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
Button {
|
||||||
|
showSubscriptionStore = true
|
||||||
|
} label: {
|
||||||
|
Text(String(localized: "subscription_required_button"))
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding()
|
||||||
|
.background(RoundedRectangle(cornerRadius: 10).fill(Color.pink))
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showSubscriptionStore) {
|
||||||
|
FeelsSubscriptionStoreView()
|
||||||
|
}
|
||||||
|
.background(
|
||||||
|
theme.currentTheme.bg
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
)
|
||||||
|
.onAppear {
|
||||||
|
EventLogger.log(event: "show_insights_view")
|
||||||
|
viewModel.generateInsights()
|
||||||
|
}
|
||||||
|
.padding(.top)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Insights Section View
|
||||||
|
struct InsightsSectionView: View {
|
||||||
|
let title: String
|
||||||
|
let icon: String
|
||||||
|
let insights: [Insight]
|
||||||
|
let textColor: Color
|
||||||
|
let moodTint: MoodTints
|
||||||
|
let imagePack: MoodImages
|
||||||
|
let theme: Theme
|
||||||
|
|
||||||
|
@State private var isExpanded = true
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
// Section Header
|
||||||
|
Button(action: { withAnimation(.easeInOut(duration: 0.2)) { isExpanded.toggle() } }) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.font(.title3)
|
||||||
|
.foregroundColor(textColor.opacity(0.7))
|
||||||
|
|
||||||
|
Text(title)
|
||||||
|
.font(.title2.bold())
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
|
||||||
|
.font(.caption.weight(.semibold))
|
||||||
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.vertical, 14)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
|
// Insights List (collapsible)
|
||||||
|
if isExpanded {
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
ForEach(insights) { insight in
|
||||||
|
InsightCardView(
|
||||||
|
insight: insight,
|
||||||
|
textColor: textColor,
|
||||||
|
moodTint: moodTint,
|
||||||
|
imagePack: imagePack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.bottom, 16)
|
||||||
|
.transition(.opacity.combined(with: .move(edge: .top)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 16)
|
||||||
|
.fill(theme.currentTheme.secondaryBGColor)
|
||||||
|
)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Insight Card View
|
||||||
|
struct InsightCardView: View {
|
||||||
|
let insight: Insight
|
||||||
|
let textColor: Color
|
||||||
|
let moodTint: MoodTints
|
||||||
|
let imagePack: MoodImages
|
||||||
|
|
||||||
|
private var accentColor: Color {
|
||||||
|
if let mood = insight.mood {
|
||||||
|
return moodTint.color(forMood: mood)
|
||||||
|
}
|
||||||
|
return textColor.opacity(0.6)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .top, spacing: 14) {
|
||||||
|
// Icon
|
||||||
|
ZStack {
|
||||||
|
Circle()
|
||||||
|
.fill(accentColor.opacity(0.15))
|
||||||
|
.frame(width: 44, height: 44)
|
||||||
|
|
||||||
|
if let mood = insight.mood {
|
||||||
|
imagePack.icon(forMood: mood)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(width: 22, height: 22)
|
||||||
|
.foregroundColor(accentColor)
|
||||||
|
} else {
|
||||||
|
Image(systemName: insight.icon)
|
||||||
|
.font(.system(size: 18, weight: .semibold))
|
||||||
|
.foregroundColor(accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text Content
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(insight.title)
|
||||||
|
.font(.subheadline.weight(.semibold))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
|
Text(insight.description)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(textColor.opacity(0.7))
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(14)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.fill(accentColor.opacity(0.08))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InsightsView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
InsightsView()
|
||||||
|
.environmentObject(IAPManager())
|
||||||
|
}
|
||||||
|
}
|
||||||
1083
Shared/Views/InsightsView/InsightsViewModel.swift
Normal file
1083
Shared/Views/InsightsView/InsightsViewModel.swift
Normal file
File diff suppressed because it is too large
Load Diff
@@ -19,25 +19,31 @@ struct MainTabView: View {
|
|||||||
let dayView: DayView
|
let dayView: DayView
|
||||||
let monthView: MonthView
|
let monthView: MonthView
|
||||||
let yearView: YearView
|
let yearView: YearView
|
||||||
|
let insightsView: InsightsView
|
||||||
let customizeView: CustomizeView
|
let customizeView: CustomizeView
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
return TabView {
|
return TabView {
|
||||||
dayView
|
dayView
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label(String(localized: "content_view_tab_main"), systemImage: "list.dash")
|
Label(String(localized: "content_view_tab_main"), systemImage: "list.dash")
|
||||||
}
|
}
|
||||||
|
|
||||||
monthView
|
monthView
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label(String(localized: "content_view_tab_month"), systemImage: "calendar")
|
Label(String(localized: "content_view_tab_month"), systemImage: "calendar")
|
||||||
}
|
}
|
||||||
|
|
||||||
yearView
|
yearView
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label(String(localized: "content_view_tab_filter"), systemImage: "line.3.horizontal.decrease.circle")
|
Label(String(localized: "content_view_tab_filter"), systemImage: "line.3.horizontal.decrease.circle")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
insightsView
|
||||||
|
.tabItem {
|
||||||
|
Label(String(localized: "content_view_tab_insights"), systemImage: "lightbulb.fill")
|
||||||
|
}
|
||||||
|
|
||||||
customizeView
|
customizeView
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label(String(localized: "content_view_tab_customize"), systemImage: "pencil")
|
Label(String(localized: "content_view_tab_customize"), systemImage: "pencil")
|
||||||
@@ -81,8 +87,9 @@ struct MainTabView: View {
|
|||||||
struct MainTabView_Previews: PreviewProvider {
|
struct MainTabView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
MainTabView(dayView: DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false)),
|
MainTabView(dayView: DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false)),
|
||||||
monthView: MonthView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: true)),
|
monthView: MonthView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: true)),
|
||||||
yearView: YearView(viewModel: YearViewModel()),
|
yearView: YearView(viewModel: YearViewModel()),
|
||||||
|
insightsView: InsightsView(),
|
||||||
customizeView: CustomizeView())
|
customizeView: CustomizeView())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"content_view_tab_main" = "Day";
|
"content_view_tab_main" = "Day";
|
||||||
"content_view_tab_month" = "Month";
|
"content_view_tab_month" = "Month";
|
||||||
"content_view_tab_filter" = "Year";
|
"content_view_tab_filter" = "Year";
|
||||||
|
"content_view_tab_insights" = "Insights";
|
||||||
"content_view_tab_customize" = "Customize";
|
"content_view_tab_customize" = "Customize";
|
||||||
"content_view_fill_in_missing_entry" = "Update %@";
|
"content_view_fill_in_missing_entry" = "Update %@";
|
||||||
"content_view_fill_in_missing_entry_cancel" = "Cancel";
|
"content_view_fill_in_missing_entry_cancel" = "Cancel";
|
||||||
|
|||||||
@@ -34,10 +34,11 @@
|
|||||||
"add_mood_header_view_title_yesterday" = "How was yesterday?";
|
"add_mood_header_view_title_yesterday" = "How was yesterday?";
|
||||||
"add_mood_header_view_title" = "How was %@?";
|
"add_mood_header_view_title" = "How was %@?";
|
||||||
|
|
||||||
"content_view_tab_main" = "Day";
|
"content_view_tab_main" = "Día";
|
||||||
"content_view_tab_month" = "Month";
|
"content_view_tab_month" = "Mes";
|
||||||
"content_view_tab_filter" = "Year";
|
"content_view_tab_filter" = "Año";
|
||||||
"content_view_tab_customize" = "Customize";
|
"content_view_tab_insights" = "Análisis";
|
||||||
|
"content_view_tab_customize" = "Personalizar";
|
||||||
"content_view_fill_in_missing_entry" = "Update %@";
|
"content_view_fill_in_missing_entry" = "Update %@";
|
||||||
"content_view_fill_in_missing_entry_cancel" = "Cancel";
|
"content_view_fill_in_missing_entry_cancel" = "Cancel";
|
||||||
"content_view_delete_entry" = "Delete this rating";
|
"content_view_delete_entry" = "Delete this rating";
|
||||||
|
|||||||
Reference in New Issue
Block a user