Skip to content

Today pull-to-sync: iOS/macOS twin of #426 + fix Android stale enabled-gate - #502

Merged
ryanbr merged 2 commits into
mainfrom
fix/ios-today-pull-to-sync-334
Jul 16, 2026
Merged

ryanbr merged 2 commits into
mainfrom
fix/ios-today-pull-to-sync-334

Conversation

@ryanbr

@ryanbr ryanbr commented Jul 16, 2026

Copy link
Copy Markdown
Owner

iOS twin of #426 (Android Today pull-to-sync), which just merged.

The gap

iOS LiquidTodayView already had a pull gesture (handlePull), but on release it only reloaded UI data (repo.refresh() + load()) — it never requested a fresh strap history sync. So an Apple user pulling on Today got a UI refresh, not the strap offload Android's pull kicks.

Fix (minimal — hook the existing gesture)

Wire model.ble.syncNow() into the existing refresh action in handlePull. Now a pull requests a manual strap offload and reloads, matching Android.

  • Same safety as Android: syncNow() is internally gated — it guards connected && bonded and no-ops if already backfilling (BLEManager:2535), the exact guard Android routes through WhoopBleClient.canRequestSync. A pull while disconnected or mid-offload safely does nothing.
  • No perf regression: added @EnvironmentObject var model: AppModel to the top view — but AppModel (unlike LiveState) doesn't publish on the ~1 Hz HR tick, so it doesn't reintroduce the per-tick Today re-render the LiveState leaves deliberately avoid.
  • Shared file → fixes macOS too. All 3 render sites (RootView, RootTabView) are under StrandApp's model injection, so the environment object always resolves.

Verification

  • App-target Swift (no default CI) → validated via app-build (below).
  • ⚠️ It's a BLE + gesture path — compile-green, but the pull-triggers-a-real-offload behavior should be confirmed on a real strap (Linux/CI can't).

Fixes #334 for Apple; Android side shipped in #426.

…not just a UI reload (#334)

Android #426 added Today pull-to-sync. iOS's LiquidTodayView already had a pull
gesture (handlePull), but it only reloaded UI data (repo.refresh + load) — it
never requested a fresh strap history sync. Wire ble.syncNow() into that existing
refresh action so a pull kicks a manual offload too, matching Android.

syncNow() is internally gated (connected + bonded + not-already-backfilling), so
a pull while disconnected or mid-offload safely no-ops — the same guard Android
routes through WhoopBleClient.canRequestSync.

Observe BLEManager (injected at the app roots beside model), NOT AppModel: AppModel
@publishes bpm on the ~1 Hz HR tick, so observing it would re-render all of Today
every second (the exact churn the LiveState leaves isolate). BLEManager only
publishes connect/discovery state, never HR.

App-target Swift (shared iOS + macOS); validated via app-build.
@ryanbr
ryanbr force-pushed the fix/ios-today-pull-to-sync-334 branch from 198593b to 0bf08b6 Compare July 16, 2026 04:46
material3 1.2.1's rememberPullToRefreshState captures the enabled lambda ONCE
(rememberSaveable, no rememberUpdatedState), so { canPullToSync } froze the plain
Boolean from the FIRST composition. Today usually first composes before the strap
has (re)connected, so the gesture stayed disabled for the whole session and
pull-to-sync (#426) silently never worked in the common cold-launch flow.

Read the stable liveSnap State live inside the enabled lambda instead, so each
gesture check sees the current connected/bonded/backfilling. Safe either way
(syncNow is triple-gated); this just lets the gesture enable once the strap is ready.

compileFullDebugKotlin + TodayExplainabilityTest green.
@ryanbr ryanbr changed the title iOS/macOS Today: pull-to-sync (twin of Android #426, fixes #334 for Apple) Today pull-to-sync: iOS/macOS twin of #426 + fix Android stale enabled-gate Jul 16, 2026
@ryanbr

ryanbr commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

Re-review of the merged Android #426 turned up a real bug, folded in here (same feature, both platforms):

Android stale pull-to-sync gate. rememberPullToRefreshState(enabled = { canPullToSync }) — material3 1.2.1 captures the enabled lambda once (rememberSaveable, no rememberUpdatedState, verified in the AAR), so it froze the plain Boolean from the first composition. Today usually first composes before the strap has (re)connected, so the gesture stayed enabled = false for the whole session — pull-to-sync silently never worked in the common cold-launch flow. It was safe (the LaunchedEffect's live re-check + syncNow's internal gate blocked any bad sync), just non-functional.

Fix: read the stable liveSnap State live inside the lambda instead of a captured Boolean, so each gesture check sees the current state. compileFullDebugKotlin + the gate test green.

@ryanbr
ryanbr merged commit b3b660d into main Jul 16, 2026
1 check passed
@ryanbr
ryanbr deleted the fix/ios-today-pull-to-sync-334 branch July 16, 2026 05:10
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
…enabled-gate (ryanbr#502)

* iOS/macOS Today: pull-to-sync — a pull now requests a strap offload, not just a UI reload (ryanbr#334)

Android ryanbr#426 added Today pull-to-sync. iOS's LiquidTodayView already had a pull
gesture (handlePull), but it only reloaded UI data (repo.refresh + load) — it
never requested a fresh strap history sync. Wire ble.syncNow() into that existing
refresh action so a pull kicks a manual offload too, matching Android.

syncNow() is internally gated (connected + bonded + not-already-backfilling), so
a pull while disconnected or mid-offload safely no-ops — the same guard Android
routes through WhoopBleClient.canRequestSync.

Observe BLEManager (injected at the app roots beside model), NOT AppModel: AppModel
@publishes bpm on the ~1 Hz HR tick, so observing it would re-render all of Today
every second (the exact churn the LiveState leaves isolate). BLEManager only
publishes connect/discovery state, never HR.

App-target Swift (shared iOS + macOS); validated via app-build.

* Android Today pull-to-sync: fix stale enabled-gate (re-review of ryanbr#426)

material3 1.2.1's rememberPullToRefreshState captures the enabled lambda ONCE
(rememberSaveable, no rememberUpdatedState), so { canPullToSync } froze the plain
Boolean from the FIRST composition. Today usually first composes before the strap
has (re)connected, so the gesture stayed disabled for the whole session and
pull-to-sync (ryanbr#426) silently never worked in the common cold-launch flow.

Read the stable liveSnap State live inside the enabled lambda instead, so each
gesture check sees the current connected/bonded/backfilling. Safe either way
(syncNow is triple-gated); this just lets the gesture enable once the strap is ready.

compileFullDebugKotlin + TodayExplainabilityTest green.
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…enabled-gate (ryanbr#502)

* iOS/macOS Today: pull-to-sync — a pull now requests a strap offload, not just a UI reload (ryanbr#334)

Android ryanbr#426 added Today pull-to-sync. iOS's LiquidTodayView already had a pull
gesture (handlePull), but it only reloaded UI data (repo.refresh + load) — it
never requested a fresh strap history sync. Wire ble.syncNow() into that existing
refresh action so a pull kicks a manual offload too, matching Android.

syncNow() is internally gated (connected + bonded + not-already-backfilling), so
a pull while disconnected or mid-offload safely no-ops — the same guard Android
routes through WhoopBleClient.canRequestSync.

Observe BLEManager (injected at the app roots beside model), NOT AppModel: AppModel
@publishes bpm on the ~1 Hz HR tick, so observing it would re-render all of Today
every second (the exact churn the LiveState leaves isolate). BLEManager only
publishes connect/discovery state, never HR.

App-target Swift (shared iOS + macOS); validated via app-build.

* Android Today pull-to-sync: fix stale enabled-gate (re-review of ryanbr#426)

material3 1.2.1's rememberPullToRefreshState captures the enabled lambda ONCE
(rememberSaveable, no rememberUpdatedState), so { canPullToSync } froze the plain
Boolean from the FIRST composition. Today usually first composes before the strap
has (re)connected, so the gesture stayed disabled for the whole session and
pull-to-sync (ryanbr#426) silently never worked in the common cold-launch flow.

Read the stable liveSnap State live inside the enabled lambda instead, so each
gesture check sees the current connected/bonded/backfilling. Safe either way
(syncNow is triple-gated); this just lets the gesture enable once the strap is ready.

compileFullDebugKotlin + TodayExplainabilityTest green.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Today: pull-to-refresh to trigger a strap sync (pairs with the #258 sync chip)

1 participant