Region-picking onboarding + per-region customization#95
Draft
kyleve wants to merge 10 commits into
Draft
Conversation
Build the deferred onboarding step that lets the user pick up to 5 US primary regions (map or searchable list, via a top segmented control) and customize each region's color/emoji/icon from predefined lists. - WhereCore: RegionColorToken / RegionAppearance / PrimaryRegion value types; SDTrackedRegion carries appearance + pick order; WhereStore primaryRegions() / setPrimaryRegion(). Picks are the tracked-region set. - WhereUI: RegionAppearanceCatalog + RegionStyleRegistry make RegionStyle data-driven (seeded by WhereSession on launch/changes, and by the widget snapshot in the widget process); PrimaryRegionSelectionModel, RegionPickerView, RegionCustomizeView, RegionsSettingsView. - OnboardingView is now an intro -> pick -> customize -> location state machine that commits picks before finishing; Settings gains a Regions editor. WidgetSnapshot carries appearances so widgets render user looks. - Tests: store round-trip/ordering, selection-model cap, registry resolution, widget appearance; RegionPickerStyle stylesheet group + docs.
Address code-review finding #1 (onboarding List had no search field because .searchable needs a navigation container) and make the onboarding and Settings region flows visually consistent: - Onboarding pick phase is a NavigationStack with a large title and a top-right Next (forward-only after the intro); its list is now filterable via .searchable. - RegionCustomizeView moves Back/Next (Done on the last region) into the nav bar with the region name as the inline title and an X-of-N indicator; callers place it in a NavigationStack. - Onboarding customize phase wraps it in a NavigationStack. - RegionsSettingsView: pick shows Cancel + Next (large title, matching onboarding), customize delegates to RegionCustomizeView's toolbar; dropped the old bottom buttons so each phase owns its own bar.
Address code-review finding #2: the commit looped per-region setPrimaryRegion/removePrimaryRegion calls, each its own store.perform, so N picks fired N changes() pings (attribution rebuild, widget republish, report reload, registry reseed each) and a mid-loop failure left partial state. Replace those single-region WhereServices/WhereStore calls with one batch setPrimaryRegions([PrimaryRegion]) that has replace semantics: upsert each entry (appearance + order) and delete any tracked row not in the list, all inside a single perform. Removals now happen by omission, so the model no longer tracks an initial set to diff against — the selection fully describes the primary set. One transaction, one ping, atomic. Tests updated to the batch API.
Address code-review finding #3. The app is US-only now (the legacy Canada/EU regions ship low-resolution polygons and aren't pickable), so seeding the editor from the default set and saving intentionally drops the non-US regions. Spell that out in PrimaryRegionSelectionModel's init(existing:) doc and pin it with a test: loading the default set into the model selects only CA/NY, and committing narrows the tracked set to those.
Address code-review finding #4: RegionStyle read from a process-wide RegionStyleRegistry.shared singleton — fine in production but a latent test-isolation hazard in the shared multi-bundle host, and it made styles update lazily rather than reactively. Replace it with a RegionStyleResolver injected through the environment (\.regionStyles), seeded by whereBroadwayRoot(regionStyles:): - the app passes WhereSession's live resolver (updated on launch + store changes), so a Settings edit / remote import now restyles the UI reactively; - the widget process builds one from WidgetSnapshot.appearances; - App Intents snippets build one from their services. The default empty resolver yields fallback looks (previews, RegionViewer). Views now read @Environment(\.regionStyles) and call regionStyles.style(for:); the global region.style / RegionStyle.style(for:) and the registry are gone. Renamed the file/test to RegionStyleResolver and updated docs.
Address code-review finding #5: unifying the bespoke look table into RegionAppearanceCatalog.defaultAppearance changed the user-visible .other glyph. Restore location.magnifyingglass (the fallback default isn't constrained to the picker's symbol catalog). EU/Canada looks are left as-is since those regions are being retired (not pickable).
Address code-review finding #6: the List disables unselected rows at the 5-region cap, but the map silently ignored a tap on a new state. Now a capped map tap fires a warning haptic (.sensoryFeedback), and an at-capacity hint appears under the selection count in both modes, so the map is as responsive/legible as the list.
Address code-review finding #7: the picker parsed all ~52 US-region GeoJSON files on appear to back Map mode. Default to List (no geometry needed) and only run the parse when Map mode is first shown (.task(id: mode)), so a List-only user never pays the cost.
Address code-review finding #8: harden RegionCustomizeView so a stale/out-of-range step index can never strand the user on the empty state. Navigation and the step indicator now use a clamped effectiveIndex; the empty (nil) state is reserved for a genuinely empty selection, which the current flow never reaches.
The test helper primary(_:_:_:) collided with the local
'let primary = try await store.primaryRegions()' variables. Local batch
compilation resolved the call to the method, but CI's compilation mode
resolved it to the [PrimaryRegion] local ('cannot call value of
non-function type'). Rename the helper to pick() to remove the ambiguity.
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.
Builds the deferred Where onboarding step: pick up to 5 US primary regions (map or filterable list, via a top segmented control), then customize each region's color / emoji / icon from predefined lists. Picks become the tracked-region set; picks + appearance persist in CloudKit-mirrored SwiftData and feed region styling. Editable later in Settings.
What's included
WhereCore — persistence & domain
RegionColorToken,RegionAppearance,PrimaryRegion.SDTrackedRegioncarries appearance (colorRaw/emoji/symbolName) +orderIndex(all optional for CloudKit — additive, no migration scaffolding).WhereStore.primaryRegions()and a single atomicsetPrimaryRegions([PrimaryRegion])(replace semantics: upserts + removals-by-omission in oneperform). Picks are the tracked set.WhereUI — flow & styling
PrimaryRegionSelectionModel(US-only, cap 5),RegionPickerView(segmented Map | List — tappable US-state polygons + searchable list),RegionCustomizeView/RegionAppearanceEditor(color/emoji/icon grids + liveRegionSummaryCardpreview),RegionsSettingsView.OnboardingViewis now anintro → pick → customize → locationstate machine that commits picks before finishing; Settings gains a Regions editor. Both reuse the same picker/customize views, wrapped inNavigationStacks with toolbar controls.@Environment(\.regionStyles)(aRegionStyleResolver), seeded bywhereBroadwayRoot(regionStyles:)— fromWhereSession(live on store changes), theWidgetSnapshotin the widget process, and services in App Intents. Fallbacks come fromRegionAppearanceCatalog. There is no globalregion.style.WidgetSnapshotcarries appearances so widgets render user choices;RegionPickerStylestylesheet group added.Notes / decisions
SDTrackedRegionsoft-delete TODO stays open (untracking still deletes the row, matching prior behavior).Follow-up review fixes (included)
This branch also folds in fixes from a self-review: nav-stack/toolbar consistency, atomic single-transaction commit, environment-injected styling (replacing a global singleton; also makes edits/imports restyle reactively), restored
.otherglyph, capped-tap haptic + hint, lazy map-geometry parsing (List is the default), and a clamped customize step index.Testing
RegionStyleResolverresolution/fallback, widget-snapshot appearance round-trip,RegionPickerStyledefaults.Stuff-iOS-Testsscheme green locally (-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2');./swiftformat --lintclean.Docs
WhereCoreandWhereUIREADME + AGENTS; resolved theRegionStylebespoke-table TODO; refreshed the now-inaccuratesetTrackedRegion(false)comment (untracking is user-reachable now).