Repository navigation
Apple Watch: Now opens alive (UX pass 3, U3) - #68
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 70918e98c7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if !ctx.hasHome { | ||
| trace.clear() |
There was a problem hiding this comment.
Clear the trace when the selected home changes
When a multi-home user switches directly from home A to home B, the incoming context still has a home, so this branch leaves A's samples in trace. Subsequent B frames are merged with those samples, and keep then persists the combined trace inside B's context; because backfill treats nearby timestamps as duplicates, A's values can even displace B's history. Compare the old and new home IDs and clear the trace and live state whenever they differ, not only on sign-out.
Useful? React with 👍 / 👎.
| case .news(.recent(let r)): | ||
| guard let ctx = context, ctx.hasHome else { return } | ||
| trace.backfill(r) |
There was a problem hiding this comment.
Scope recent-history messages to their home
A delayed .recent message sent while home A was active is accepted whenever the watch currently has any home, including after an application context has switched it to home B. Unlike .now, WatchRecent carries no site ID for this handler to validate, so A's readings are backfilled and immediately cached under B. Include the originating home in the recent payload and reject it unless it matches context.home.
Useful? React with 👍 / 👎.
With its reply to a look the phone sends each node's last three minutes (WatchNews.recent, packed, 8 KB at most); the watch draws them in ink and keeps them. A short status line that never shrinks or cuts, the space rule in WatchRoot, direction marks, 30 pt rows, the mode by name, a horizontal drag that reads the trace, the forecast while the watch looks, the mode receipt back on Now, the redacted shape before the first reading, and a cache where a nil record keeps the old one and only a sign-out forgets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a watch looks, the phone's stream runs as for a screen: a watchers count in SiteModel holds 1 Hz and FTW Cloud's looking, and the phone going to the background closes nothing until the look ends. The watch's glow lasts as long as the phone counts a reading live (WatchNow.standsMs) and 2.5 of the stream's own intervals plus the hop, at least 4 s, so a frame at 1 Hz, 5 s or one late never makes Now blink, and a real silence still goes dark. The watch renews its look with the frames every 20 s and once when it cools, and the phone takes its time in the background anew with each renewal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
b5df2d4 to
22d89ca
Compare
Package U3 from the third UX review (
design/ftw-ios-polish-2026-10/ux-pass-3/ux-pass-3.md, section 6; findings 4, 10 (the root rule and Now), 12, 31, 32 (the trace), 34, 37, 38, 62 and 65). It runs beside U4, which owns the charts,NodeViewandPriceView; this PR does not touch them. U8 comes next and builds on thePhosphor.swiftandWatchWire.swiftchanges here.On 0.4.3 the watch's Now opened with one dot where the trace should be, half a trace after two minutes, and a status line that shrank to 77 % and still cut "FTW IN CONTR…" when not live.
What changes
The last three minutes. With its reply to
.lookthe phone now sends each node's last three minutes from its live ring (WatchNews.recent, aWatchRecent) before the first.now, and again when the watch looks again while the phone still forwards. Each lane ist0, the steps in ms and whole signed watts, packed as JSON then LZFSE inside the message, as the context already is.WatchRecent.makekeeps every other reading until the whole message fits 8 KB. Measured: five nodes of noisy readings at 1 Hz take 5.1 KB and the demo's 1.7 KB, with nothing thinned; ten readings a second get thinned to 6.5 KB. The watch draws them in ink (seen, not live) and live frames light the line from the right as before. A reading the watch already has keeps its place, so what it saw live stays seen. The watch keeps the trace in its own cache (WatchContext.recent, which the phone never fills) each time it keeps the context and as soon as the phone's minutes arrive, so a cached open shows the whole three minutes too. While not live the window ends at the newest reading and holds still; the status line dates it.Status line. A watch form that never shrinks or cuts:
● LIVE · FTW IN CONTROL,○ 07:00 · FTW IN CONTROL,● DEMO · FTW STYR. No "AS OF" and no seconds; the hollow ring and the minute are the age. One SF Mono size for every state, set once from the screen's width so the longest ordinary line in the phone's language fits (WatchGlance.statusChars: the reading lines, "no reading yet" and the receipt's steps). That is 10 pt on 40 mm and 13 pt on 46 mm in English, 11 and 13 pt in Swedish. A warning that runs longer takes a second line rather than lose its words. The status's parts keep their own tone, so only the warning is red.Space (
pageSpace()inWatchRoot.swift). No navigation title on Now; the clock stays. The content runs into the bottom safe area and stops 8 pt short of the edge. Plan, Car and Price keep their titles: U8 applies the same rule to them.Small figures. The battery and the grid carry a direction mark beside their glyph in ink: ▲ while the battery stores or the home buys, ▼ while it gives or sells, as the trace draws them. Each row is at least 30 pt tall and half the width to hit. A short line in the node's hue under the large figure's glyph marks the traced node; it is ink while the trace is.
Mode. The toolbar button shows the mode by name, as the phone does: "Plan" while the planner runs the home, otherwise the manual mode's name. A choice on its way shows at once.
Finger. A horizontal drag on the trace holds the window still, shows the reading under the finger in the large figure with its direction word, ticks
.selectionper reading (a light impact at the high, the low and the ends of a run) and draws the lock-on with the reading's time in the tag. The gesture is simultaneous and decides on the first 6 pt: a drag that starts up or down is left to the pages.Sun arc.
WatchShare.lookingasks for the plan with its forecast, so the dial has its sun without the phone's plan view ever opening. The watch keeps the last sun until a newer forecast replaces it.Mode receipt. After the confirm the watch goes back to Now, and the receipt takes the status line's row (
SENDING…,SENT ▸,SENT ▸ ACCEPTED ▸,SENT ▸ CONFIRMED, or▲ REFUSED,▲ NOT CONFIRMED,▲ NOT SENT) until 4 s after the box answers, as on the phone. One wait, no timer. The mode list still shows the full receipt.No reading yet. Now's shape, redacted, under
○ NO READING YET, as Car does.-FTWWatchNoReading YES(debug builds) shows it for pictures.Cache.
WatchContext.merging(_:over:): only an explicit sign-out (signedOut, sent when the phone has no home and no paired site) forgets. A context with no home that does not say so comes from a phone still opening its home and changes nothing. For the same home each record is the newer of the two, and one the phone did not send keeps the old. Both new fields are optional, so the 0.4.3 cache still opens.The trace's scale. The watch's floors are a quarter of the phone's (500, 750 and 925 W), since its trace is about a quarter as tall: 60 W of noise spreads as few points as on the phone, and a home using 400 W fills the trace instead of leaving a 32 pt band above it.
40 mm. At 197 pt tall Now did not fit and fell back to scrolling. On the lowest screens the large figure is 27 pt instead of 32, the gaps 2 pt and the trace's least height 16 pt; the rows keep their 30 pt.
Now no longer blinks with the phone in a pocket
Fredrik saw the watch flip between LIVE and waiting. With the phone locked,
SiteModel.hide()asked the box for 0.2 Hz and told FTW Cloud nobody looked;WatchBridgepassed each frame on as it landed, so one came every 5 s, and the watch stayed lit 4 s after each. Four seconds lit, one dark, over and over. The phone going to the background also closed the session mid-look, and nothing renewed the look before the phone's time in the background or its 2 minutes ran out.SiteModelcounts watching watches (beginWatching/endWatching, fromWatchShare.looking).setVisibleandhidecount a watching watch as someone who sees the stream: 1 Hz from the box, looking on FTW Cloud, and reads as for a screen.AppModel.suspendcloses nothing while a watch looks; the end of the look suspends, as the intents' runner already does.WatchGlance.Glow). A frame stays lit as long as the phone itself counts it live (WatchNow.standsMs, its gap rule: 3.5 s at 1 Hz) plus 1 s for the hop, and at least 2.5 of the stream's last intervals plus the hop, never under 4 s. One late frame no longer goes dark. A real silence still does, after the phone's own time for a reading. Without the phone's word, no frame stays lit past 16 s.Evidence:
aSteadyStreamNeverBlinks: frames at 1 s and at 5 s with 50–700 ms of hop, one 2.5 s late at 1 Hz and one 6 s late at 0.2 Hz: not one 50 ms step dark.aRealSilenceGoesDark: 10 s of nothing is dark.aLookingWatchKeepsTheStreamAtOneHertz: the demo hidden gives 6 frames or fewer in 20 s; with a watch looking, 17 or more, also after the app goes to the background; the end of the look drops it back. With the watchers count switched off, the test fails.Not checked on real hardware: whether iOS gives a locked phone fresh time in the background with each message from the watch. If it does not, the look ends when that time runs out, the watch goes dark once and asks again.
Pictures
Demo, watchOS 27 simulator, Apple Watch Series 11 46 mm and SE 3 40 mm, paired with an iPhone 17 Pro. "0.4.3" pictures are from the review at 2918419; the watch's code had not changed on
mainsince. "First frame" is the first picture after launch, 0.8 s in, with the phone in reach.46 mm, English: 0.4.3 open, after 2 min and cached, then this branch: first frame, live, cached (phone off), quiet, no reading yet

46 mm, Swedish

40 mm, English

40 mm, Swedish

The first second of an open with the phone in reach (46 mm, English): the kept trace in ink from the first frame, the phone's three minutes joined, lit from the next live frame. Swedish · 40 mm

Mode receipt back on Now (from

testModeReceiptOnNow)Finger on the trace: under the finger, at the oldest reading, after the lift, and a swipe up that pages (from

testDragAcrossTheTraceKeepsThePage)Sun arc with the phone's plan view never opened

Every single picture is under
u3/46andu3/40. The Swedish quiet picture at 46 mm comes from a build before the last change to the line's clearance from the page dots (10 pt to 8 pt); its type size is the same. The other pictures come from this head.Bands
tools/bands.pyon Now at 46 mm, live, English: no band of 24 pt or more. With--min 10: 18 pt above the clock (the system's), then 15, 17 and 15.5 pt between the small rows and at the foot. 0.4.3 had 44 and 38 pt.Tests
swift test: 669 green (239 in FTWShared, 430 in FTWKit), onmainwith iPhone: the box session goes through FTW Cloud, the relay as fallback (one door, step 2) #67.theLastThreeMinutesFitOneReply(five nodes at 1 Hz fit 8 KB, come back to the ms and the watt, round-trip as news),aWindowTooDenseIsThinnedToFit,aNilRecordKeepsTheOldOne,onlyASignOutForgets,theDialKeepsTheLastSunAndTheTraceItsNewest,aContextKeptByTheLastBuildStillOpens.WatchShareTests: the demo's reply to a look (about one reading a second across three minutes, under 8 KB), the forecast while the watch looks, no home says why.testDragAcrossTheTraceKeepsThePagepasses on 46 and 40 mm,testModeReceiptOnNowon 46 mm.The drag and the pages: unchecked on a real watch
The check that a horizontal drag on the trace and the vertical pages do not fight ran only in the simulator, with XCUITest's synthesised touches: a slow drag across the trace read it and left the page alone, and a swipe up from the middle of the screen, which is the trace, turned the page. A slow synthesised drag up from the trace did not page; a quick swipe did. No real watch was used, so this stays unchecked on a wrist. Haptics, Always On and the Crown are unchecked too.
Not in this PR
.nowframes carry their arrival time and the ring carries the box's stamp, so the first live frame may sit up to a few hundred ms after the same reading from the ring: a flat step of the same value, not a jump.No launch path on the phone changed, so there are no
LaunchClocknumbers.🤖 Generated with Claude Code