Apple Watch app: complications, data grid, background delivery#580
Open
MtlPhil wants to merge 36 commits intoloopandlearn:apple-watchfrom
Open
Apple Watch app: complications, data grid, background delivery#580MtlPhil wants to merge 36 commits intoloopandlearn:apple-watchfrom
MtlPhil wants to merge 36 commits intoloopandlearn:apple-watchfrom
Conversation
- Make SmallFamilyView right slot configurable via Live Activity settings - Add Unit.displayName to GlucoseSnapshot for consistent unit labelling - Use ViewThatFits for adaptive CarPlay vs Watch Smart Stack layout - Fix APNs push token lost after renewal-overlay foreground restart - Fix Not Looping overlay not showing when app is backgrounded - Rename Live Activity settings section headers
…#576) startIfNeeded() unconditionally reused any existing activity, which meant that on cold start (app killed while stale overlay was showing) willEnterForeground is never sent, handleForeground never runs, and viewDidAppear → startFromCurrentState → startIfNeeded just rebinds to the stale activity — leaving the overlay visible. Fix: before reusing an existing activity in startIfNeeded(), check whether its staleDate has passed or the renewal window is open. If so, end it (awaited) and call startIfNeeded() again so a fresh activity with a new 7.5h deadline is started. Also add cancelRenewalFailedNotification() to handleForeground() so the "Live Activity Expiring" system notification is dismissed whenever the foreground restart path fires, not only via forceRestart(). Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…andlearn#577) * Fix LA not refreshing on foreground after stale overlay startIfNeeded() unconditionally reused any existing activity, which meant that on cold start (app killed while stale overlay was showing) willEnterForeground is never sent, handleForeground never runs, and viewDidAppear → startFromCurrentState → startIfNeeded just rebinds to the stale activity — leaving the overlay visible. Fix: before reusing an existing activity in startIfNeeded(), check whether its staleDate has passed or the renewal window is open. If so, end it (awaited) and call startIfNeeded() again so a fresh activity with a new 7.5h deadline is started. Also add cancelRenewalFailedNotification() to handleForeground() so the "Live Activity Expiring" system notification is dismissed whenever the foreground restart path fires, not only via forceRestart(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix stale LA dismissed by iOS incorrectly blocking auto-restart When iOS dismisses a Live Activity because its staleDate passed (background stale overlay case), laRenewalFailed is false, so the state observer's else branch fired and set dismissedByUser=true — permanently blocking all auto-restart paths (startFromCurrentState has guard !dismissedByUser). Fix 1: attachStateObserver now checks staleDatePassed alongside laRenewalFailed; both are iOS-initiated dismissals that should allow auto-restart. Fix 2: handleForeground() Task resets dismissedByUser=false before calling startFromCurrentState(), guarding against the race where the state observer fires .dismissed during our own end() call before its Task cancellation takes effect. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Bump json 2.18.0 → 2.19.3 (CVE-2026-33210) and faraday 1.8.0 → 1.10.5 (CVE-2026-25765) to resolve Dependabot alerts loopandlearn#12 and loopandlearn#13.
…gem-updates Fix Dependabot security alerts for json and faraday gems
…n#581) * Add separate Watch and CarPlay toggles to Live Activity settings Use GeometryReader in LockScreenFamilyAdaptiveView to distinguish Watch Smart Stack (height ≤ 75 pt) from CarPlay Dashboard (height > 75 pt) at render time — both surfaces share ActivityFamily.small with no API to tell them apart, so canvas height is the only runtime signal. Adds la.watchEnabled and la.carPlayEnabled App Group keys (default true). The Right Slot picker hides when both are disabled. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix Watch/CarPlay toggle detection: use width, not height; use Color.black Two bugs with the height-based threshold: 1. System padding pushes Watch Smart Stack canvas above 75 pt, causing both Watch and CarPlay to be classified as CarPlay (blank on both when CarPlay toggle is off). 2. Color.clear is transparent — cached Watch renders show through it, leaving stale data visible after the toggle is turned off. Fix: switch to width-based detection. Watch Ultra 2 (widest model) is ~183 pt; CarPlay is always at least ~250 pt. A 210 pt threshold gives a ~14% buffer above the max Watch width. Replace Color.clear with Color.black so old frames are fully covered when the widget is disabled. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix stale overlay tap: endingForRestart flag prevents dismissedByUser race Root cause: handleForeground() clears laRenewalFailed=false synchronously before calling activity.end(). When the state observer fires .dismissed, renewalFailed is already false and staleDatePassed may also be false, so it falls into the user-swipe branch and sets dismissedByUser=true. Fix 4 (dismissedByUser=false in the Task) was meant to override this, but the state observer's MainActor write can be queued *after* the Task's reset write, winning the race and leaving dismissedByUser=true. The result: LA stops after tapping the overlay and never restarts. Add endingForRestart flag set synchronously (on the MainActor) before end() is called. The state observer checks it first — before renewalFailed or staleDatePassed — so any .dismissed delivery triggered by our own end() call is never misclassified as a user swipe, regardless of MainActor queue order. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Use forceRestart on Watch/CarPlay toggle instead of refreshFromCurrentState The LA must be ended and recreated for Watch/CarPlay content changes to take effect immediately. refreshFromCurrentState only sends a content update to the existing activity; forceRestart ends the activity and starts a fresh one, so the widget extension re-evaluates and the black/clear tile appears (or disappears) without APNs latency. Note: true per-surface dismissal (tile fully gone from Watch OR CarPlay while the other remains) requires splitting into two LAs and is a future architectural change. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Remove Watch/CarPlay toggles; redesign expanded Dynamic Island Watch/CarPlay toggles removed: disfavoredLocations API requires iOS 26 and GeometryReader-based detection is unreliable. Reverts to always showing SmallFamilyView on .small activity family. Expanded Dynamic Island redesigned to match SmallFamilyView layout: - Leading: glucose + trend arrow (colour-keyed, firstTextBaseline), delta below - Trailing: configurable slot (same smallWidgetSlot setting as CarPlay/Watch) with label + value, replaces hardcoded IOB/COB - Bottom: unchanged — "Updated at" or "Not Looping" banner Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add Future Carbs Alert Notify when a future-dated carb entry's scheduled time arrives, serving as a reminder to start eating in pre-bolus scenarios. Tracks future carb entries across alarm ticks using persistent storage, with configurable max lookahead window (default 45 min) to filter out fat/protein entries and minimum carb threshold (default 5g). * Add FutureCarbsCondition unit tests 10 test cases covering tracking, firing, deletion, lookahead bounds, min grams filter, past carbs, stale cleanup, multi-carb per-tick behavior, and duplicate prevention. Also fix Tests target missing FRAMEWORK_SEARCH_PATHS for CocoaPods dependencies and add missing latestPumpBattery field in withBattery test helper. * Fix future-dated treatments not being downloaded from Nightscout Trio sets created_at to the scheduled future time, but the Nightscout query had an upper bound of "now", excluding any future-dated entries. Extend the query window by predictionToLoad minutes so treatments within the graph lookahead are fetched. Also add addingMinutes parameter to getDateTimeString for precise minute-level offsets. * Default Future Carbs Alert to acknowledge instead of snooze * Download treatments 6 hours into the future Replace the prediction-based lookahead with a fixed 6-hour window and rename currentTimeString to endTimeString for clarity. * Fix max lookahead sliding window bug Carbs originally outside the lookahead window could drift into it over time and fire incorrectly. Now all future carbs are tracked from first observation, and only fire if their original distance (carbDate minus observedAt) was within the max lookahead. Stale cleanup also preserves entries whose carb still exists to prevent re-observation with a fresh timestamp.
* Add commit guidelines and best practices to README.md * Add pull request guidelines to README --------- Co-authored-by: codebymini <daniel@codebymini.se>
…n#585) Three clearly separated .dismissed sources: (a) endingForRestart — our own end() during planned restart, ignore (b) iOS system force-dismiss — renewalFailed OR pastDeadline (now >= laRenewBy) → auto-restart on next foreground, laRenewBy preserved (c) User decision — explicit swipe → dismissedByUser=true, laRenewBy=0 (renewal intent cancelled) Remove staleDatePassed: staleDate expiry fires .ended not .dismissed. Preserve laRenewBy on .ended and system .dismissed so handleForeground() detects the renewal window and restarts on next foreground. Only the user-swipe path clears laRenewBy, preventing handleForeground() from re-entering the renewal path after the user explicitly killed the LA. Fix handleForeground() nil-current path: reaching it means iOS ended the LA while the renewal window was open (laRenewBy still set). A user-swipe would have cleared laRenewBy to 0, so overlayIsShowing would be false and this branch would never be reached — startFromCurrentState() is safe. Set renewalWarning to 30 minutes (overlay appears 30 min before 7.5h deadline). Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add removal dates to migration step comments Document when each migration step reached main and when it can safely be removed (one year after its first release). * Fix step 3 removal date and improve comment wording Step 3 was first released in v4.5.0 (2026-02-01), not v5.0.0. Also use consistent "Released in" wording across all steps.
…bolus Show time and value when tapping carb, bolus and SMB dots
* Open statistics from home screen by tapping the stats area Closes loopandlearn#563 Modal (home screen tap) shows Done button, menu uses navigation push with native back chevron, and tab shows only Refresh. --------- Co-authored-by: codebymini <daniel@codebymini.se>
…oopandlearn#584) * Improve GRIView layout and axis label positioning in GRIRiskGridView * Update label for hypoglycemia component in GRIView
# Conflicts: # LoopFollow.xcodeproj/project.pbxproj # LoopFollow/ViewControllers/MainViewController.swift
Dismissing the LA was silently killing Watch data: refreshFromCurrentState guarded on !dismissedByUser before calling performRefresh, so the Watch received nothing after a dismiss. Fix: - Remove the LA guard from refreshFromCurrentState — Watch and store must update regardless of LA state. - In performRefresh, capture the dedup result (snapshotUnchanged) before saving to the store, then run store save + Watch send unconditionally. The laEnabled/dismissedByUser/dedup guards now gate only the LA update. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Brings all Watch complications, Watch app, WatchConnectivity, and background delivery work on top of the current live-activity branch tip. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
This PR adds a fully functional Apple Watch app and two watchOS complications to LoopFollow, built on top of the
live-activitybranch. It shares the sameGlucoseSnapshotdata model and App Group container as the Live Activity so all surfaces stay in sync with zero duplication.What's included
watchOS complications (graphicCorner + graphicCircular)
Complication 1 — Gauge Corner
Complication 2 — Stack Corner
delta | ⇢projectedwhen a projected value is available, otherwise just delta--Both complications open the Watch app on tap.
Watch app (4 swipeable tabs)
Tab 1 — Glucose view
Tabs 2–N — Data grid pages
Last tab — Slot selection
Data flow
Reliability design decisions
sendMessage(instant, foreground only) +transferUserInfo(guaranteed, background). Both sent on every update so neither is a single point of failure.updateApplicationContext: always updated alongsidetransferUserInfo. The Watch reads it on session activation and in every background refresh task — ensures data is available even if background task budget is exhausted.sessionReachabilityDidChange: iPhone pushes immediately when Watch app opens, so the first screen is never stale.watchAck(timestamp) back to iPhone after processing each snapshot. iPhone logs a warning when Watch ACK is >600 s behind, making missed deliveries visible in logs.setTaskCompletedWithSnapshot(false)is called insideDispatchQueue.main.async, afterCLKComplicationServer.reloadTimeline()— prevents watchOS from suspending the extension before ClockKit receives the reload request.Key files
LoopFollowWatch Watch App/LoopFollowWatchApp.swiftLoopFollowWatch Watch App/ContentView.swiftLoopFollow/WatchComplication/ComplicationEntryBuilder.swiftLoopFollow/WatchComplication/WatchComplicationProvider.swiftLoopFollow/WatchComplication/WatchSessionReceiver.swiftLoopFollow/WatchComplication/WatchFormat.swiftLoopFollow/WatchConnectivity/WatchConnectivityManager.swiftLoopFollow/LiveActivity/GlucoseSnapshot.swiftLoopFollow/LiveActivity/LAAppGroupSettings.swift🤖 Generated with Claude Code