Skip to content

Apple Watch: Now opens alive (UX pass 3, U3) - #68

Merged
frahlg merged 2 commits into
mainfrom
claude/ios-u3-watch
Oct 15, 2026
Merged

frahlg merged 2 commits into
mainfrom
claude/ios-u3-watch

Conversation

@frahlg

@frahlg frahlg commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

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, NodeView and PriceView; this PR does not touch them. U8 comes next and builds on the Phosphor.swift and WatchWire.swift changes 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 .look the phone now sends each node's last three minutes from its live ring (WatchNews.recent, a WatchRecent) before the first .now, and again when the watch looks again while the phone still forwards. Each lane is t0, the steps in ms and whole signed watts, packed as JSON then LZFSE inside the message, as the context already is. WatchRecent.make keeps 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() in WatchRoot.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 .selection per 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.looking asks 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; WatchBridge passed 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.

  • The phone holds 1 Hz while a watch looks. SiteModel counts watching watches (beginWatching/endWatching, from WatchShare.looking). setVisible and hide count 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.suspend closes nothing while a watch looks; the end of the look suspends, as the intents' runner already does.
  • The glow follows the stream's own pace (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.
  • The look renews itself. The watch tells the phone it still looks every 20 s while frames arrive, and once more when the glow cools. Each word takes the phone's time in the background anew. The phone resends the three minutes only after 10 s without forwarding a frame.

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.
  • A 75 s recording on the paired simulators, from this branch before it took in iPhone: the box session goes through FTW Cloud, the relay as fallback (one door, step 2) #67 (the same watch and phone code). FTW starts in front, and at 10 s Settings opens and puts FTW in the background (the phone's screen showed "◀ FTW"). The status dot was lit in all 756 frames sampled at 10 per second; a lit and a dark reference frame check the detector. The watch's own kept trace shows the frames it got while the phone was in the background: 151 from 19:00:25 to 19:03:01, a median of 1.01 s apart and 1.33 s at most. Video

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 main since. "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/46 and u3/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.py on 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), on main with iPhone: the box session goes through FTW Cloud, the relay as fallback (one door, step 2) #67.
  • Wire: 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.
  • Glance: the short status line, one width in every state, the receipt mark by mark, direction marks, the reading under a finger.
  • 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.
  • Watch UI tests on the simulator: testDragAcrossTheTraceKeepsThePage passes on 46 and 40 mm, testModeReceiptOnNow on 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

  • Plan, Car and Price keep their titles and their bottom inset (U8).
  • The phone's .now frames 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.
  • A phone app killed and relaunched while the watch looks does not get a new look until the watch's reachability changes or it comes to the front again. Seen in the simulator, where reachability does not flip; on a wrist the next raise asks again.

No launch path on the phone changed, so there are no LaunchClock numbers.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T16:42:42.821333Z 70918e9 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +156 to +157
if !ctx.hasHome {
trace.clear()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +119 to +121
case .news(.recent(let r)):
guard let ctx = context, ctx.hasHome else { return }
trace.backfill(r)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

frahlg and others added 2 commits October 15, 2026 19:04
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>
@frahlg
frahlg force-pushed the claude/ios-u3-watch branch from b5df2d4 to 22d89ca Compare October 15, 2026 17:11
@frahlg
frahlg merged commit 4b595e7 into main Oct 15, 2026
1 check passed
@frahlg frahlg mentioned this pull request Oct 15, 2026
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.

1 participant