Restyle Where Settings as iOS-style drill-in screens with search - #111
Merged
Conversation
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
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.
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.
What
Reworks the Where Settings tab from one long inline
Forminto 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
Listblocks (matching the iOS Settings app's grouped layout):A typed
SettingsListSectionowns the blocking (block order = on-screen order), with a test guarding that everySettingsDestinationappears in exactly one block.Behind the rows:
LocationSettingsView,AlertsSettingsView,AppearanceSettingsView,VisibleYearSettingsView,BackupSettingsView,DataSettingsView), each owning the section(s) moved out of the oldSettingsView.SettingsDestination.isSheetdrives push-vs-sheet routing for both group rows and search results, with no baredefault:.Search
enum Item: SettingsItem(CaseIterable+title/keywords) and derives its ownsearchResults;SettingsCatalogconcatenates them (one registration point)..searchablefilters by title/keywords withContentUnavailableView.searchon empty.SettingsRoute(a focused route is constructible only from aSettingsSearchResult, 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
SettingsFocuscan only be built from aSettingsItem, so a strayAnyHashablecan't be routed; per-screenItemtokens are namespaced by type, so no central id registry or collision check is needed.destination(for:)switch,SettingsDestination.isSheet, andSettingsListSectioncoverage are all exhaustive / test-guarded, so adding a group is a compile error (or a failing test) until it's wired.searchResultsrather than iterating a[any SettingsSection.Type], which sidesteps a SILGen crash when dispatching a static requirement (or a\.searchResultskey path) through an existential metatype.Other
SettingsStylemotion token toWhereStylesheet(flash animation/duration/scroll-settle delay; colors stay inline per the "no adaptive/accent colors in the sheet" rule).BackupModel(mirroring the error alert) so it survives the Backup screen being popped mid-import.Testing
SettingsSearchTests(index completeness, focus-token uniqueness, title/keyword matching, route construction, list-section coverage).ScreenHostingTestshosts each new sub-screen (including one with afocus:set);BackupModelTestscover the new import-success state;WhereStylesheetTestspin the newSettingsStyletoken../swiftformat --lintclean and the fullStuff-iOS-Testsscheme passes on iOS 26 simulator.