Skip to content

iPhone: the plan dial as one turn, shared with the watch, and its widget (WP11) - #53

Merged
frahlg merged 1 commit into
mainfrom
claude/ios-wp11-dial
Oct 14, 2026
Merged

frahlg merged 1 commit into
mainfrom
claude/ios-wp11-dial

Conversation

@frahlg

@frahlg frahlg commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Work package WP11 of the iPhone polish release ("Phosphor"). The plan view opens on the plan dial: one turn for the next 24 hours, drawn by the same face as the Apple Watch dial from #43. The dial gets its own widget, for StandBy and the lock screen. This replaces #27, which drew the dial before the new palette and the chart engine existed.

A finger round the dial, in the demo on an iPhone 17 Pro Max

The same recording as MP4.

Fredrik's decisions, as built

  • One turn is the next 24 hours: 00 at the bottom, 12 at the top, so the day's light sits in the upper half.
  • 96 price ticks in eight levels. Cyan stores, ink gives power, ink3 waits; gold belongs to the sun alone.
  • The sun's arc on the rim comes from the box's forecast (GET /api/mpc/plan, slots at 10 W or more). No forecast, no arc: never astronomy.
  • A dotted violet arc for the car, with its goal at the end.
  • A hand at now, and a 2° gap in the rim at the seam where +24 h meets now. The ticks fade toward the seam.
  • The time and the price in the middle, and a third line kept for the quarter under a finger (LAGRAR · 3,0 KW · BILEN).
  • No battery ring and no glass disc in the app. The widget keeps a dark ground.
  • The dial is the plan view's default. A two-way switch in the toolbar (a dial and a lanes icon, read by VoiceOver as Tavla and Banor) brings the lanes one tap away, and the phone keeps the viewer's choice (FTWPlanStyle).

One face for phone, widget and watch

  • WatchDial became PlanDial (FTWShared/Charts/PlanDial.swift), with the sun, the seam, the rule for a finger and the words a finger reads, made once per plan and minute.
  • DialFace moved from FTWWatchShared to FTWCharts/PlanDialFace.swift and split in two: PlanDialFace (every still mark, Equatable) and PlanDialHand (the hand and the lock-on). PlanDialView puts them together for the watch app, the watch's complication and the phone's widget. The watch gains the sun's arc and the seam's gap; its Crown, words and complications are unchanged.
  • PlanEntry carries daylight, so the sun travels to the widget and the watch with the plan. An older record without it still reads.
  • The quarter running now reads the plan slot that holds the rest of it. Core starts its first slot the moment it plans, inside a quarter, so before this the dial (on the watch too) drew that quarter as waiting with no plan.

The finger

A drag round the ring (a ring-shaped hit area, so a drag from the middle scrolls the page) costs one lookup per event (PlanDial.index(x:y:from:)) and writes nothing a view reads until the quarter changes. Then the hand jumps to it, brackets close on its tick, the middle shows that quarter, .selection ticks with the 40 ms floor, and a light impact marks now, the cheapest two hours' edges and the car's goal. The finger cannot wind past the seam: from the end it stays on the last quarter. On lift the hand springs back to now along the day, with Theme.spring. VoiceOver steps it an hour at a time.

From #27 this keeps the scrub path (one lookup per event, haptics beside the marks, signposts "dial drag" and "dial quarter"), the widget kind energy.ftw.plan-dial, PlanEntry.daylight, the style switch and its UI test. It drops the two turns, the battery ring, the glass disc and the legend.

Widget

energy.ftw.plan-dial, systemSmall (also StandBy) and accessoryCircular. Small: the dial, the price now in the middle and PLANERAD 21:36 under it, always, on a dark ground. Circle: the dial and the price now. Stores and the car are .widgetAccentable() for tinted and clear screens. The timeline steps a quarter at a time for 12 hours; an hour after the box made the plan the dial goes grey.

Departures from the design document

  1. The hours sit just outside the rim, as on the watch, not inside it, where the dearest ticks would cover them.
  2. The widget's middle shows the price now, not the time: the lock screen and StandBy already show the time, and the ticks are prices.
  3. Under the dial: the plan's sentence, the CHEAPEST line, PLANERAD 16:51 · PLANERAS OM VAR 15:E MIN, then the event rows as plain rows on the ground. The three new sentences and dropping the rows wait for Fredrik's answer to the plan design pass.
  4. The lanes stay on Swift Charts. They can move to the engine once WP10 (Now's strip) lands.
  5. The status line stays in the subtitle on every screen (WP8), so the planned time sits under the dial instead.

Evidence

  • swift test: FTWShared 191 and FTWKit 341 tests green. New PlanDialTests (the seam, a finger finds its quarter, the seam stops a finger, the sun from the forecast and none without it, daylight from the forecast's slots, the middle's words, the quarter running now reads a plan the box made inside it, the planned line, an old record still reads, Swedish for every word). WatchTests pass unchanged against the renamed type.
  • The watch app builds for the watchOS simulator with the shared face.
  • UI PlanDialTests on the iPhone 17 Pro Max, iOS 26.5: 3 of 3 (VoiceOver reads the dial at now; a finger round the ring keeps the page still; the lanes are one tap away and the choice is kept across launches).
  • NowFitsTests on the iPhone SE (3rd generation): 10 of 11 in the full run, under a load average near 500 from other simulators on the laptop. testNowHoldsItsShapeWhileReadingsArrive once saw the sun's node 2.5 pt lower 6 s on; it passed in two runs on its own. This PR does not change Now.
  • Frame log in the SE simulator (-FTWDialScrub YES -FTWFrameLog YES), a finger once round the ring and back in 12 s: 191 quarter changes (signpost "dial quarter"), 60 frames a second and 0 ms of late frames in every second of the turn.
  • StandBy cannot be entered in the simulator, so its pictures are rendered on the Mac from the widget's own face.

Not done here: a recording from Fredrik's iPhone against the home box, StandBy and haptics on a device, and an Animation Hitches run. The simulator refuses Animation Hitches and the SwiftUI instrument.

Pictures

iPhone 17 Pro Max: dark, light and the largest text size (accessibility XXXL):

The plan view on an iPhone 17 Pro Max

iPhone SE (3rd generation): dark, light and the largest text size:

The plan view on an iPhone SE

The lanes, one tap away:

The plan view in lanes

The widget on the home screen, dark and light. It keeps its dark ground in both:

The dial widget on the home screen

The lock screen's circle, placed by LockScreenTour (TEST_RUNNER_FTW_LOCK=1 TEST_RUNNER_FTW_LOCK_PAGE=2), and enlarged:

The dial's circle on the lock screen

StandBy by day and at night, rendered on the Mac from the widget's own face:

StandBy by day
StandBy at night

Closes #27.

🤖 Generated with Claude Code

…get (WP11)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 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-14T17:12:16.171431Z c80a7cd 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: c80a7cd381

ℹ️ 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 +98 to +99
let sun = self.plan.forecast.first.map { "\(self.plan.forecast.count)@\($0.startMs)" } ?? "-"
return "\(plan?.rev ?? 0)|\(plan?.slots.first?.startMs ?? 0)|\(plan?.slots.count ?? 0)|\(plan?.stale ?? false)|\(w.joined(separator: ";"))|\(p)|\(sun)"

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 Persist forecast changes when they arrive

When the plan and forecast requests start together, PlanModel.load() can invoke plan.received and check() before loadForecast() assigns forecastStamp; the forecast assignment has no callback to PlanShare, so adding it to this key does not cause another shared-record write. A fresh installation can therefore persist daylight: nil, leaving the new widget and watch dial without the solar arc until an unrelated plan or site event triggers check(); invoke sharing when the forecast is accepted and include the sunlight values in the change key.

Useful? React with 👍 / 👎.

}

let unit = PriceUnits.unit(currency)
func figure(_ minor: Double) -> String { Words.decimal(PriceStrip.text(abs(minor), currency)) }

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 Preserve the sign of negative dial prices

For a negative tariff, this helper converts the value to its magnitude before it is used by both prices and cheapestText, so −5 is shown as 5 ÖRE/KWH and the cheapest annotation also describes a positive price. price(_:) and figure(_:) retain the sign, meaning the phone dial disagrees with the watch and widget precisely when the customer is being paid to consume; retain the sign here unless the magnitude is accompanied by an explicit below-zero label.

Useful? React with 👍 / 👎.

@frahlg
frahlg merged commit d07ba16 into main Oct 14, 2026
1 check passed
@frahlg frahlg mentioned this pull request Oct 14, 2026
frahlg added a commit that referenced this pull request Oct 14, 2026
BlockBars keeps W1.1's highlight and takes PlanDial.levels from #53.
DialStrip, the rectangle's strip, now reads PlanDial.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg added a commit that referenced this pull request Oct 14, 2026
Since #53 the watch's plan page draws the shared face at the app size,
and on a dial about 100 pt across the goal ("80 %") landed on the hand's
time and price in the middle. The goal now shows only on a dial at least
200 pt across, which the phone's always is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg added a commit that referenced this pull request Oct 14, 2026
…raphic complications (#49)

* Apple Watch W1.1: Now alive on one screen, pages without scrolling, and graphic complications

Fredrik's notes on W1 (14 Oct 2026): the watch widgets let text run
outside and should hold little text; the app should have the phone's
live view and pages instead of scrolling.

- Now: the status line, one node's figure large, the phone's live
  trace (LiveSeries drawn by LiveTraceSurface in TraceInk), glowing
  only while live, and the other nodes small. A tap on a small one
  traces it. The mode sits behind a toolbar button.
- Every page fits one screen at the standard sizes; at text sizes too
  large for the watch it scrolls rather than cut a reading off. The
  car's commands are one row of glyph buttons.
- Complications: no sentences. Rectangular is a glyph, one time, the
  price now and the plan unrolled as a strip; corner is the price with
  the day's range as a gauge; inline is the price; the cheap card is a
  glyph, its hours, one figure and the hours ahead with the window
  standing out. Every text has one line and may shrink.
- LiveTraceSurface takes `paused`, so a trace that is not live holds
  still. WatchNow.Reading gives its signed watts for the trace, and
  each node the phone's scale floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Watch W1.1: the car page fits one screen, the dial's line says what comes next, Now waits for live in the tour

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Watch W1.1: the car page fits one screen, and the strip is DialStrip

The receipt now takes the place of what the car does, its goal and next
window, until the page is left; a settled receipt then goes. The page no
longer keeps a room of its own for the receipt, so it fits 46 mm without
scrolling and the swipe turns the page. The commands are one row of glyph
buttons, 30 pt high.

The watch's strip view is now DialStrip, so it does not shadow the
PlanStrip value that the phone's plan lane adds to FTWShared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Watch W1.1: Now and the car fit 41 mm

Now's trace takes the height that is left, at least 24 pt, so the page
fits 41 mm without scrolling and the swipe turns the page. On 46 mm the
trace is as tall as before.

The car's meter goes under its level when both do not fit on one line,
as with "20 % antaget", rather than cutting the word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Watch W1.1: the car's goal stays off a watch-sized dial

Since #53 the watch's plan page draws the shared face at the app size,
and on a dial about 100 pt across the goal ("80 %") landed on the hand's
time and price in the middle. The goal now shows only on a dial at least
200 pt across, which the phone's always is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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