From 134197f6077001c014ea01f0ed360b3a30fbb04a Mon Sep 17 00:00:00 2001 From: ryanbr Date: Thu, 9 Jul 2026 11:11:58 +1200 Subject: [PATCH] =?UTF-8?q?Widget:=20keep=20the=20Home-screen=20widget=20f?= =?UTF-8?q?resh=20=E2=80=94=20publish=20on=20backgrounding=20+=20live=20ba?= =?UTF-8?q?ttery/connection=20(#114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The iOS widget snapshot only republished on a FOREGROUND refreshSeq bump (analyze tick / backfill / health sync / .active). Two gaps made it lag the app: - Strap battery % and connection are LIVE state (model.live), not repo-cache, so they never bumped refreshSeq — the widget battery never moved while the app was open ("battery not updating"). Now republished on those changes, foreground-gated (budget-exempt; both low-frequency so no throttle needed). - Nothing republished on the way out, so the widget could show a state older than the user last saw. Now publishes once on background/resign (the app runs bluetooth-central so it isn't suspended; one reload per app-exit is budget-safe). WidgetKit is still a periodic glance in the background (reload budget) — this narrows the app-vs-widget gap, it doesn't make it real-time. The "Charge reads like battery" label + richer illustrations from #114 are separate design calls, not here. StrandiOS target — can't build on WSL; a testing build verifies the Swift compiles. --- StrandiOS/App/StrandiOSApp.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/StrandiOS/App/StrandiOSApp.swift b/StrandiOS/App/StrandiOSApp.swift index dc82843fe..4c664fc85 100644 --- a/StrandiOS/App/StrandiOSApp.swift +++ b/StrandiOS/App/StrandiOSApp.swift @@ -122,6 +122,19 @@ struct StrandiOSApp: App { // so a refresh storm can't burn the ~50/day complication transfer budget. Task { await watch.pushLatest(from: model) } } + // #114: strap battery % and connection are LIVE (model.live), not repo-cache, so they never + // bump refreshSeq — the widget's battery would otherwise never move while the app is open + // (the "battery not updating" report). Republish on those too, foreground-gated. Both are + // low-frequency (battery ~every 8 min; connection flips are rare), so no throttle is needed + // and foreground-initiated reloads are budget-exempt. dropFirst() skips the attach replay. + .onReceive(model.live.$batteryPct.dropFirst()) { _ in + guard scenePhase == .active else { return } + Task { await WidgetSnapshot.publish(from: model) } + } + .onReceive(model.live.$connected.dropFirst()) { _ in + guard scenePhase == .active else { return } + Task { await WidgetSnapshot.publish(from: model) } + } // #581: the `noop://import-health` deep link the iOS Shortcut opens after building the // HealthKit-free payload. Filter on the host so other future schemes don't trip the // importer; macOS never registers the scheme so this stays iOS-only. @@ -163,6 +176,10 @@ struct StrandiOSApp: App { await watch.pushLatest(from: model) } } else if phase == .background { + // #114: capture the LAST in-app live state on the way out so the Home widget matches what + // the user just saw — its battery/HR/score otherwise lag to the last FOREGROUND refreshSeq + // bump. One reload per app-exit is low-frequency and well within WidgetKit's daily budget. + Task { await WidgetSnapshot.publish(from: model) } // #155: refresh the Documents/noop_sync.txt drop file the user's Siri Shortcut logs // into Apple Health. Gated inside writeIfEnabled on the opt-in default (OFF) — a // no-op until the user turns on Shortcuts Export.