Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-02-01 17:57:34 -06:00
parent 319bad27a1
commit 938b2bc033

View File

@ -9,6 +9,7 @@ struct RootView: View {
@State private var selectedTab: RootTab @State private var selectedTab: RootTab
@State private var analyticsPrewarmTask: Task<Void, Never>? @State private var analyticsPrewarmTask: Task<Void, Never>?
@State private var isForegroundRefreshing = false @State private var isForegroundRefreshing = false
@State private var isResumingFromBackground = true
private let foregroundRefreshMinimumSeconds: TimeInterval = 0.15 private let foregroundRefreshMinimumSeconds: TimeInterval = 0.15
private let debugForegroundRefreshMinimumSeconds: TimeInterval = 0.8 private let debugForegroundRefreshMinimumSeconds: TimeInterval = 0.8
private let debugForegroundRefreshKey = "debugForegroundRefreshNextForeground" private let debugForegroundRefreshKey = "debugForegroundRefreshNextForeground"
@ -38,6 +39,10 @@ struct RootView: View {
self.settingsStore = settingsStore self.settingsStore = settingsStore
self.categoryStore = categoryStore self.categoryStore = categoryStore
self._selectedTab = State(initialValue: initialTab) self._selectedTab = State(initialValue: initialTab)
// Update time-of-day immediately before any views render.
// This ensures correct rituals are shown when app resumes from background.
store.updateCurrentTimeOfDay()
} }
var body: some View { var body: some View {
@ -46,6 +51,7 @@ struct RootView: View {
Tab(String(localized: "Today"), systemImage: "sun.max.fill", value: RootTab.today) { Tab(String(localized: "Today"), systemImage: "sun.max.fill", value: RootTab.today) {
NavigationStack { NavigationStack {
TodayView(store: store, categoryStore: categoryStore) TodayView(store: store, categoryStore: categoryStore)
.id(store.currentTimeOfDay)
} }
} }
@ -83,10 +89,17 @@ struct RootView: View {
} }
.transition(.opacity) .transition(.opacity)
} }
// Brief overlay when resuming to hide stale snapshot
if isResumingFromBackground {
AppSurface.primary
.ignoresSafeArea()
}
} }
.tint(AppAccent.primary) .tint(AppAccent.primary)
.background(AppSurface.primary.ignoresSafeArea()) .background(AppSurface.primary.ignoresSafeArea())
.animation(.easeInOut(duration: 0.12), value: isForegroundRefreshing) .animation(.easeInOut(duration: 0.12), value: isForegroundRefreshing)
.animation(.easeIn(duration: 0.05), value: isResumingFromBackground)
.onChange(of: scenePhase) { _, newPhase in .onChange(of: scenePhase) { _, newPhase in
if newPhase == .active { if newPhase == .active {
store.reminderScheduler.clearBadge() store.reminderScheduler.clearBadge()
@ -95,6 +108,12 @@ struct RootView: View {
// This ensures the correct rituals are shown without a visible transition. // This ensures the correct rituals are shown without a visible transition.
store.updateCurrentTimeOfDay() store.updateCurrentTimeOfDay()
// Hide resume overlay after a tiny delay to allow view to update
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(50))
isResumingFromBackground = false
}
let useDebugOverlay = UserDefaults.standard.bool(forKey: debugForegroundRefreshKey) let useDebugOverlay = UserDefaults.standard.bool(forKey: debugForegroundRefreshKey)
if useDebugOverlay { if useDebugOverlay {
UserDefaults.standard.set(false, forKey: debugForegroundRefreshKey) UserDefaults.standard.set(false, forKey: debugForegroundRefreshKey)
@ -106,6 +125,9 @@ struct RootView: View {
showOverlay: useDebugOverlay, showOverlay: useDebugOverlay,
minimumSeconds: useDebugOverlay ? debugForegroundRefreshMinimumSeconds : foregroundRefreshMinimumSeconds minimumSeconds: useDebugOverlay ? debugForegroundRefreshMinimumSeconds : foregroundRefreshMinimumSeconds
) )
} else if newPhase == .background {
// Prepare for next resume
isResumingFromBackground = true
} }
} }
.onChange(of: selectedTab) { _, _ in .onChange(of: selectedTab) { _, _ in