Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
9ade3b00ea
commit
2eb2abfba8
@ -21,6 +21,8 @@ final class RitualStore: RitualStoreProviding {
|
||||
private var cachedDatesWithActivity: Set<Date> = []
|
||||
private var cachedPerfectDayIDs: Set<String> = []
|
||||
private var pendingReminderTask: Task<Void, Never>?
|
||||
private var insightCardsNeedRefresh = true
|
||||
private var cachedInsightCards: [InsightCard] = []
|
||||
|
||||
/// Reminder scheduler for time-slot based notifications
|
||||
let reminderScheduler = ReminderScheduler()
|
||||
@ -401,7 +403,19 @@ final class RitualStore: RitualStoreProviding {
|
||||
}
|
||||
|
||||
func insightCards() -> [InsightCard] {
|
||||
return PerformanceLogger.measure("RitualStore.insightCards") {
|
||||
refreshInsightCardsIfNeeded()
|
||||
return cachedInsightCards
|
||||
}
|
||||
|
||||
func refreshInsightCardsIfNeeded() {
|
||||
guard insightCardsNeedRefresh else { return }
|
||||
cachedInsightCards = PerformanceLogger.measure("RitualStore.insightCards") {
|
||||
computeInsightCards()
|
||||
}
|
||||
insightCardsNeedRefresh = false
|
||||
}
|
||||
|
||||
private func computeInsightCards() -> [InsightCard] {
|
||||
// Only count habits from active arcs for today's stats
|
||||
let activeHabitsToday = habitsActive(on: Date())
|
||||
let totalHabits = activeHabitsToday.count
|
||||
@ -481,7 +495,6 @@ final class RitualStore: RitualStoreProviding {
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
func createQuickRitual() {
|
||||
let defaultDuration = 28
|
||||
@ -675,6 +688,7 @@ final class RitualStore: RitualStoreProviding {
|
||||
|
||||
private func invalidateAnalyticsCache() {
|
||||
analyticsNeedsRefresh = true
|
||||
insightCardsNeedRefresh = true
|
||||
}
|
||||
|
||||
private func computeDatesWithActivity() -> Set<Date> {
|
||||
|
||||
@ -87,9 +87,16 @@ struct HistoryMonthView: View {
|
||||
|
||||
private var dayGrid: some View {
|
||||
let columns = Array(repeating: GridItem(.flexible(), spacing: Design.Spacing.xSmall), count: 7)
|
||||
let dates = daysInMonth
|
||||
let progressValues = PerformanceLogger.measure("HistoryMonthView.progressValues.\(monthTitle)") {
|
||||
dates.map { date -> Double? in
|
||||
guard let date else { return nil }
|
||||
return date > today ? 0 : completionRate(date, selectedRitual)
|
||||
}
|
||||
}
|
||||
|
||||
return LazyVGrid(columns: columns, spacing: Design.Spacing.xSmall) {
|
||||
ForEach(Array(daysInMonth.enumerated()), id: \.offset) { index, date in
|
||||
ForEach(Array(dates.enumerated()), id: \.offset) { index, date in
|
||||
if let date = date {
|
||||
let isToday = calendar.isDate(date, inSameDayAs: today)
|
||||
let isFuture = date > today
|
||||
@ -97,7 +104,7 @@ struct HistoryMonthView: View {
|
||||
// Show all days - future days show with 0 progress and are not tappable
|
||||
HistoryDayCell(
|
||||
date: date,
|
||||
progress: isFuture ? 0 : completionRate(date, selectedRitual),
|
||||
progress: progressValues[index] ?? 0,
|
||||
isToday: isToday,
|
||||
isFuture: isFuture,
|
||||
onTap: { if !isFuture { onDayTapped(date) } }
|
||||
|
||||
@ -19,21 +19,27 @@ struct HistoryView: View {
|
||||
@Bindable var store: RitualStore
|
||||
@State private var selectedRitual: Ritual?
|
||||
@State private var selectedDateItem: IdentifiableDate?
|
||||
@State private var showingExpandedHistory = false
|
||||
@State private var monthsToShow = 2
|
||||
@State private var refreshToken = UUID()
|
||||
@State private var cachedProgressByDate: [Date: Double] = [:]
|
||||
|
||||
private let calendar = Calendar.current
|
||||
private let baseMonthsToShow = 2
|
||||
private let monthChunkSize = 6
|
||||
|
||||
/// Generate months based on expanded state
|
||||
/// - Collapsed: Last month + current month (2 months)
|
||||
/// - Expanded: Up to 12 months of history
|
||||
/// Months are ordered oldest first, newest last (chronological order)
|
||||
private var months: [Date] {
|
||||
PerformanceLogger.measure("HistoryView.months") {
|
||||
let today = Date()
|
||||
let currentMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: today)) ?? today
|
||||
|
||||
// Determine how far back to go
|
||||
let monthsBack = showingExpandedHistory ? 11 : 1 // 12 months or 2 months total
|
||||
let totalAvailableMonths = totalMonthsAvailable(from: currentMonth)
|
||||
let effectiveMonthsToShow = min(monthsToShow, totalAvailableMonths)
|
||||
let monthsBack = max(0, effectiveMonthsToShow - 1)
|
||||
guard let startMonth = calendar.date(byAdding: .month, value: -monthsBack, to: currentMonth) else {
|
||||
return [currentMonth]
|
||||
}
|
||||
@ -49,13 +55,15 @@ struct HistoryView: View {
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if there's more history available beyond what's shown
|
||||
private var hasMoreHistory: Bool {
|
||||
guard let earliestActivity = store.earliestActivityDate() else { return false }
|
||||
let today = Date()
|
||||
guard let twoMonthsAgo = calendar.date(byAdding: .month, value: -1, to: today) else { return false }
|
||||
return earliestActivity < twoMonthsAgo
|
||||
guard let oldestVisibleMonth = months.first else { return false }
|
||||
let oldestVisibleStart = calendar.startOfDay(for: oldestVisibleMonth)
|
||||
let earliestStart = calendar.startOfDay(for: earliestActivity)
|
||||
return earliestStart < oldestVisibleStart
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@ -73,7 +81,8 @@ struct HistoryView: View {
|
||||
month: month,
|
||||
selectedRitual: selectedRitual,
|
||||
completionRate: { date, ritual in
|
||||
store.completionRate(for: date, ritual: ritual)
|
||||
let day = calendar.startOfDay(for: date)
|
||||
return cachedProgressByDate[day] ?? store.completionRate(for: day, ritual: ritual)
|
||||
},
|
||||
onDayTapped: { date in
|
||||
selectedDateItem = IdentifiableDate(date: date)
|
||||
@ -93,8 +102,20 @@ struct HistoryView: View {
|
||||
if let selectedRitual {
|
||||
self.selectedRitual = newRituals.first { $0.id == selectedRitual.id }
|
||||
}
|
||||
refreshProgressCache()
|
||||
refreshToken = UUID()
|
||||
}
|
||||
.onChange(of: selectedRitual) { _, _ in
|
||||
refreshProgressCache()
|
||||
refreshToken = UUID()
|
||||
}
|
||||
.onChange(of: monthsToShow) { _, _ in
|
||||
refreshProgressCache()
|
||||
refreshToken = UUID()
|
||||
}
|
||||
.onAppear {
|
||||
refreshProgressCache()
|
||||
}
|
||||
.sheet(item: $selectedDateItem) { item in
|
||||
HistoryDayDetailSheet(
|
||||
date: item.date,
|
||||
@ -116,13 +137,18 @@ struct HistoryView: View {
|
||||
if hasMoreHistory || showingExpandedHistory {
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: Design.Animation.standard)) {
|
||||
showingExpandedHistory.toggle()
|
||||
if monthsToShow > baseMonthsToShow {
|
||||
monthsToShow = baseMonthsToShow
|
||||
} else {
|
||||
let totalAvailableMonths = totalMonthsAvailable(from: Date())
|
||||
monthsToShow = min(monthsToShow + monthChunkSize, totalAvailableMonths)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: Design.Spacing.xSmall) {
|
||||
Text(showingExpandedHistory ? String(localized: "Show less") : String(localized: "Show more"))
|
||||
Text(monthsToShow > baseMonthsToShow ? String(localized: "Show less") : String(localized: "Show more"))
|
||||
.font(.subheadline)
|
||||
Image(systemName: showingExpandedHistory ? "chevron.up" : "chevron.down")
|
||||
Image(systemName: monthsToShow > baseMonthsToShow ? "chevron.up" : "chevron.down")
|
||||
.font(.caption)
|
||||
}
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
@ -168,6 +194,43 @@ struct HistoryView: View {
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||
}
|
||||
|
||||
private func refreshProgressCache() {
|
||||
Task { @MainActor in
|
||||
await Task.yield()
|
||||
let snapshotMonths = months
|
||||
let selected = selectedRitual
|
||||
let cache = PerformanceLogger.measure("HistoryView.progressCache") {
|
||||
var result: [Date: Double] = [:]
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
|
||||
for month in snapshotMonths {
|
||||
guard let firstOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: month)),
|
||||
let range = calendar.range(of: .day, in: .month, for: firstOfMonth) else {
|
||||
continue
|
||||
}
|
||||
|
||||
for day in range {
|
||||
guard let date = calendar.date(byAdding: .day, value: day - 1, to: firstOfMonth) else { continue }
|
||||
let normalizedDate = calendar.startOfDay(for: date)
|
||||
guard normalizedDate <= today else { continue }
|
||||
result[normalizedDate] = store.completionRate(for: normalizedDate, ritual: selected)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
cachedProgressByDate = cache
|
||||
}
|
||||
}
|
||||
|
||||
private func totalMonthsAvailable(from currentMonth: Date) -> Int {
|
||||
guard let earliestActivity = store.earliestActivityDate() else { return baseMonthsToShow }
|
||||
let earliestMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: earliestActivity)) ?? currentMonth
|
||||
let comps = calendar.dateComponents([.month], from: earliestMonth, to: currentMonth)
|
||||
let months = (comps.month ?? 0) + 1
|
||||
return max(baseMonthsToShow, months)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
|
||||
@ -32,10 +32,15 @@ struct InsightsView: View {
|
||||
endPoint: .bottomTrailing
|
||||
))
|
||||
.onAppear {
|
||||
Task {
|
||||
await Task.yield()
|
||||
store.refreshAnalyticsIfNeeded()
|
||||
store.refreshInsightCardsIfNeeded()
|
||||
}
|
||||
}
|
||||
.onChange(of: store.rituals) { _, _ in
|
||||
store.refreshAnalyticsIfNeeded()
|
||||
store.refreshInsightCardsIfNeeded()
|
||||
refreshToken = UUID()
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ struct RootView: View {
|
||||
@Bindable var categoryStore: CategoryStore
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@State private var selectedTab: RootTab
|
||||
@State private var analyticsPrewarmTask: Task<Void, Never>?
|
||||
|
||||
/// The available tabs in the app.
|
||||
enum RootTab: Hashable {
|
||||
@ -87,6 +88,15 @@ struct RootView: View {
|
||||
let refreshStart = CFAbsoluteTimeGetCurrent()
|
||||
store.refresh()
|
||||
PerformanceLogger.logDuration("RootView.refreshCurrentTab.store.refresh", from: refreshStart)
|
||||
analyticsPrewarmTask?.cancel()
|
||||
if selectedTab != .insights {
|
||||
analyticsPrewarmTask = Task { @MainActor in
|
||||
try? await Task.sleep(for: .milliseconds(350))
|
||||
guard !Task.isCancelled else { return }
|
||||
store.refreshAnalyticsIfNeeded()
|
||||
store.refreshInsightCardsIfNeeded()
|
||||
}
|
||||
}
|
||||
if selectedTab == .settings {
|
||||
let settingsStart = CFAbsoluteTimeGetCurrent()
|
||||
settingsStore.refresh()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user