Skip to content

Restyle Where Settings as iOS-style drill-in screens with search - #111

Merged
kyleve merged 9 commits into
mainfrom
cursor/ios-style-settings
Jul 20, 2026
Merged

Restyle Where Settings as iOS-style drill-in screens with search#111
kyleve merged 9 commits into
mainfrom
cursor/ios-style-settings

Conversation

@kyleve

@kyleve kyleve commented Jul 20, 2026

Copy link
Copy Markdown
Owner

What

Reworks the Where Settings tab from one long inline Form into an iOS-Settings-style top level: grouped blocks of icon drill-in rows, each opening its own focused screen, plus a search field that filters individual settings and deep-links to them.

Structure

Seven top-level groups, split into headerless List blocks (matching the iOS Settings app's grouped layout):

  • Location · Regions
  • Alerts & Data Resolution (reminder, daily summary, issue alerts, and the GPS drift threshold / find-issues scan — reminders are just another kind of alert, so they live on this one page)
  • Appearance · Visible Year
  • Backup · Data

A typed SettingsListSection owns the blocking (block order = on-screen order), with a test guarding that every SettingsDestination appears in exactly one block.

Behind the rows:

  • Most groups are pushed sub-screens (LocationSettingsView, AlertsSettingsView, AppearanceSettingsView, VisibleYearSettingsView, BackupSettingsView, DataSettingsView), each owning the section(s) moved out of the old SettingsView.
  • Regions and App Icon are presented as sheets (editor/commit flows, so an explicit Cancel/Save boundary is clearer and matches the app's other feature flows). SettingsDestination.isSheet drives push-vs-sheet routing for both group rows and search results, with no bare default:.
  • Top-level rows show a value subtitle where it's cheap (Location status, the report year).

Search

  • Decentralized index: each screen owns a local enum Item: SettingsItem (CaseIterable + title/keywords) and derives its own searchResults; SettingsCatalog concatenates them (one registration point). .searchable filters by title/keywords with ContentUnavailableView.search on empty.
  • Tapping a result deep-links via a type-safe SettingsRoute (a focused route is constructible only from a SettingsSearchResult, so a "wrong screen + foreign item" pairing can't be spelled). The target screen scrolls the row into view and briefly flashes it (SettingsFocusScope / .settingsRow), honoring Reduce Motion.

Type-safety notes

  • SettingsFocus can only be built from a SettingsItem, so a stray AnyHashable can't be routed; per-screen Item tokens are namespaced by type, so no central id registry or collision check is needed.
  • The pushed destination(for:) switch, SettingsDestination.isSheet, and SettingsListSection coverage are all exhaustive / test-guarded, so adding a group is a compile error (or a failing test) until it's wired.
  • The catalog concatenates each concrete screen's searchResults rather than iterating a [any SettingsSection.Type], which sidesteps a SILGen crash when dispatching a static requirement (or a \.searchResults key path) through an existential metatype.

Other

  • Added a SettingsStyle motion token to WhereStylesheet (flash animation/duration/scroll-settle delay; colors stay inline per the "no adaptive/accent colors in the sheet" rule).
  • New localized strings for row/group titles, the search prompt, and per-setting keywords.
  • Backup import-success confirmation now lives on BackupModel (mirroring the error alert) so it survives the Backup screen being popped mid-import.

Testing

  • New SettingsSearchTests (index completeness, focus-token uniqueness, title/keyword matching, route construction, list-section coverage).
  • ScreenHostingTests hosts each new sub-screen (including one with a focus: set); BackupModelTests cover the new import-success state; WhereStylesheetTests pin the new SettingsStyle token.
  • ./swiftformat --lint clean and the full Stuff-iOS-Tests scheme passes on iOS 26 simulator.

kyleve added 9 commits July 19, 2026 19:41
Rework the Settings tab from one long inline Form into an iOS-Settings-style
top-level list of icon drill-in rows plus a search field.

- Eight grouped sub-screens (Location, Regions, Reminders, Alerts & Data
  Resolution, Appearance, Visible Year, Backup, Data), each its own view;
  Regions and App Icon convert from sheets to push destinations.
- Decentralized search: each screen owns a local `enum Item: SettingsItem`
  (CaseIterable + title/keywords) and derives its own `searchResults`;
  `SettingsCatalog` concatenates them. `.searchable` filters by title/keywords
  with `ContentUnavailableView.search` on empty.
- Search deep-links: a type-safe `SettingsRoute` (destination + `SettingsFocus`,
  focus buildable only from a result) drives scroll-to + a one-shot row flash
  (`SettingsFocusScope` / `.settingsRow`), honoring Reduce Motion.
- Add a `SettingsStyle` motion token to WhereStylesheet (colors stay inline);
  new localized strings for row/group titles, prompt, and per-setting keywords.
- Tests: host each sub-screen, plus SettingsSearchTests for the index; pin the
  new stylesheet token.

Note: results are concatenated per concrete screen rather than iterated over a
`[any SettingsSection.Type]` to sidestep a SILGen crash dispatching a static
requirement through an existential metatype.
- Import-success confirmation now lives on `BackupModel` (`lastImportSummary` /
  `isShowingImportSuccess`), mirroring the error alert, so it survives the Backup
  screen being popped mid-import. Covered by BackupModelTests.
- Present Regions and App Icon as sheets again (they're editor/commit flows, so
  an explicit Cancel/Save boundary is clearer and matches the app's other
  feature flows) instead of pushing them onto the Settings stack. A new
  `SettingsDestination.isSheet` drives push-vs-sheet routing for both the group
  rows and search results, with no bare `default:`.
- Document why the location permission alert lives on the Location screen (only
  the Grant button / tracking toggle set `permissionDenied`; an external
  Settings-app toggle flows through the authorization observer, which never does).
Group the eight top-level rows into four headerless List sections
(Location/Regions, Reminders/Alerts, Appearance/Visible Year, Backup/Data),
matching the iOS Settings app's grouped-block layout instead of one long list.

Add a typed `SettingsListSection` (order = on-screen order) with a test that
every `SettingsDestination` appears in exactly one block.
Reminders are just another kind of alert, so fold the daily-logging-reminder
section into `AlertsSettingsView` (as the first section) and drop the separate
`.reminders` destination and `RemindersSettingsView`. The merged page keeps the
"Alerts & Data Resolution" title and now covers: reminder, daily summary, issue
alerts, and data resolution / find issues.

`RemindersSettingsModel` (the model) is unchanged and still drives the reminder
section; only the standalone view is removed.
CI's SwiftFormat docComments rule requires a doc comment (///) for the
comment preceding the @entry environment property; use one.
Search results now render their group's SF Symbol beside the setting name +
parent-group subtitle, so a result reads as belonging to its section.
On the Alerts & Data Resolution page, give the drift-threshold picker and the
manual issue scan their own sections, each with a header and an explanatory
footer. The threshold picker's row is relabeled "Drift threshold" (the section
header now carries "Data resolution").
Wrap each top-level row's (and search result's) SF Symbol in a rounded-square
chip with a per-section color (SettingsDestination.iconColor); the glyph is
white in light mode and black in dark mode. Chip geometry lives in
WhereStylesheet.SettingsStyle.
@kyleve
kyleve merged commit 412ab9d into main Jul 20, 2026
2 checks passed
kyleve pushed a commit that referenced this pull request Jul 26, 2026
Weekly read-only audit refresh. Every open July 19 finding was re-verified against current source, and the week's new surface was reviewed: the Periscope migration (#94), Settings drill-in (#111), developer HUD (#115), navigation restructure (#119), log-viewer tooling (#107), String Catalog symbols (#124), the Gregorian-calendar pass (`fe99dde`), preview coverage (`52f0136`), Bumper Bowling (#127), and catalog serialization (#135). No source changed.

## Structural changes to the audit

- **`LogKit` and `LogViewerUI` are gone** — Periscope replaced them, so their sections are removed and the inventory drops to **14 SPM library targets**. Counts refreshed to ~359 source / ~198 test files (WhereUI 84 → 113, WhereCore 70 → 87, PeriscopeTools 15 → 24, RegionKit 9 → 13).
- New section for **Bumper Bowling**, the repo-owned architecture lint that landed this week.

## Two new high findings

1. **`where.gregorian_calendar` enforces nothing.** The rule filters `MemberAccessExprSyntax` on `base == "Calendar"`, so it matches a spelled-out `Calendar.current` but not the implicit-member form (`calendar: Calendar = .current`, `in: .current`) — which, after the Gregorian call-site pass, is the only form left in the tree. CI hard-gates `bumper lint` at `severity: .error` and is green, which confirms it finds nothing while seven production sites drift. `.bumper/RULES.md` also claims three calendar violations and some preview-coverage violations are "left visible during this bootstrap"; both claims are stale.
2. **`CalendarDay.displayDate` resolves through `Calendar.current`.** `startOfDay(in:)` interprets the day's Gregorian Y-M-D as *that* calendar's components, so on a Buddhist-era device every day label flowing through the helper — relabel, logged days, resolution details, the region drill-in — renders a date ~543 years off. `DateRangeFormatting.abbreviated` and `PresenceTimeline.stints` also default to `.current`.

The three highs carried from July 19 are all still open: daily-summary staleness, the WhereUI tracking-toggle race, and the LifecycleKit terminal-phase race (now described precisely — `runSteps`' cancellation check sits at the *top* of the loop, so a cancel during the **final** step's `minVisible` hold is never observed before it returns `.completed`).

## Retired as verified fixed

The Where app `README.md`; the cold-launch reason misclassification (`.undetermined` + `completedStepIDs`); `#Preview` coverage across WhereUI/WhereWidgets; the hand-maintained string-key facades; the in-app SwiftData browser; the widget post-midnight snapshot policy (now documented as intentional degradation); and `SharedItemLoader` warning logs.

## TODOs.md

**`Where/TODOs.md`** — retires the `WhereModel` break-up (now 185 lines of process-lifetime state), the SwiftData browser, the `@_spi(Testing)` migration, and two items whose subject no longer exists (the `WhereModel` "controller" guard, hoisting `Calendar.current` onto the controller). Files this week's findings, pins the exact remaining `waitForOneRunloop` call sites, and promotes soft-deleting untracked regions now that the shipped picker reaches the hard delete.

**`Shared/Periscope/TODOs.md`** — adds a P1 (the orphan sweep closing spans whose `survivesRelaunch` policy couldn't be decoded, silently) and three P2s (`LogInspectorModel` re-querying full subtrees per commit, `SpanHistoryModel` swallowing an `SpanEnded` decode failure, and a test pinning open-span containment). Sharpens the two density items with the user-visible consequence.

**Root `AGENTS.md`** — its description of the audit cited the LogKit/LogViewerUI sections as an example of stale content surviving a refresh; this refresh removed them, so the caveat is rewritten to keep the useful half without an example that goes stale again.

## Verification

Static analysis only — the Cloud agent runs Linux, so no `tuist test`, `bumper lint`, or simulator runs. CI status on `main` was read via `gh` (green) which is what lets the "the Gregorian rule finds nothing" conclusion stand. Changes are markdown-only, so `./swiftformat --lint` and `./xcstrings --lint` are unaffected.
cursor Bot pushed a commit that referenced this pull request Aug 9, 2026
All WhereUI/Where-scoped open items checked against HEAD. The module went
113 -> 224 sources since the July 26 audit, so most citations had moved.

Closed five, four of them from the broken-snapshots cluster:
- The blank VoiceOver calendar captures: re-recorded by PR #196, LFS size
  fields now 3.2 MB / 3.6 MB against the 66 KB / 171 KB that evidenced the
  solid-white blanks. A 3.2 MB PNG is not blank. Verified from LFS pointers
  only; noted that confirming pixels needs a macOS ./test --review.
- The timeline accent bar: no longer applicable, PR #200 deleted StintRow and
  both stylesheet tokens. Closed as obsolete, not fixed -- nobody scaled the
  capsule, the capsule is gone.
- The timeline ax5 squish: fixed the way the item asked, via
  timeline.row.stacksDayCount at accessibility sizes.
- The Resolve sheet ax5 cutoff: now .fullContentScreenDefaults.
- The CalendarYearGrid capture-time scroll skip: the last product-code read of
  isCapturingSnapshot that wasn't a stand-in is gone.

Recorded a trap on the three that stay open: PR #196 re-recorded their
references with the layout code unchanged, so those images now pin the same
defect at a new size. Re-check the image before fixing.

Sharpened rather than restated:
- The calendar item now names exactly four production sites, separates eight
  DEBUG preview/fixture sites, and explicitly excludes calendar.timeZone =
  .current, which is the correct pattern and has been miscounted before.
- The notification-prompt item now carries the full launch chain and the root
  cause: all three preferences default to true on a fresh install, so the fix
  has to reckon with the defaults, not just the call site.
- WhereSession is 636 lines, not the ~460 filed -- it grew, because PR #160
  landed multi-device recording on it.

Filed two new findings, and corrected one of them while verifying: three
screens lack image coverage, but only RemovedDeviceView is new -- Alerts and
VisibleYear have been uncovered since PR #111, so prior audits missed them.
Also nested the evidence feature-discovery VoiceOver gap under the existing
widget-gallery item rather than filing a second item for the same defect class.

Did not file the timeline's missing report.calendar separately; it is the same
defect as the calendar item and is now cited there.

Validation: docs only. swiftformat --lint and attribution --check pass.
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