From 4935f1ab3c7a2b1b6c85f4a76bd784f0ea1e7f9c Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sun, 6 Sep 2026 19:30:33 -0700 Subject: [PATCH 1/3] fix(WhereUI): fade welcome scrim independently of card motion --- .../Primary/LocationWelcomeOverlay.swift | 63 +++++++++++-------- .../Sources/Primary/LocationsView.swift | 14 ++--- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/Where/WhereUI/Sources/Primary/LocationWelcomeOverlay.swift b/Where/WhereUI/Sources/Primary/LocationWelcomeOverlay.swift index 7008e9095..3b12312bc 100644 --- a/Where/WhereUI/Sources/Primary/LocationWelcomeOverlay.swift +++ b/Where/WhereUI/Sources/Primary/LocationWelcomeOverlay.swift @@ -4,7 +4,7 @@ import UIKit /// The modal scrim and adaptive placement for a Locations welcome card. struct LocationWelcomeOverlay: View { - let presentation: LocationWelcomeModel.Presentation + let presentation: LocationWelcomeModel.Presentation? let dismissAction: () -> Void let planStayAction: ((Region) -> Void)? @@ -12,37 +12,46 @@ struct LocationWelcomeOverlay: View { @Environment(\.stylesheet) private var stylesheet var body: some View { + // Keep the container mounted so the scrim never inherits the card's motion. ZStack { - Color.black - .opacity(stylesheet.locationWelcome.scrimOpacity) - .ignoresSafeArea() - .accessibilityHidden(true) + if let presentation { + Color.black + .opacity(stylesheet.locationWelcome.scrimOpacity) + .ignoresSafeArea() + .accessibilityHidden(true) + .transition(.opacity) + .zIndex(0) - GeometryReader { proxy in - ScrollView { - RegionWelcomeCard( - presentation: presentation, - dismissAction: dismissAction, - planStayAction: planStayAction, - ) - .fixedSize(horizontal: false, vertical: true) - .padding(.horizontal, stylesheet.spacing.xxxLarge) - .padding(.vertical, stylesheet.spacing.xxxLarge) - .frame(maxWidth: .infinity, minHeight: proxy.size.height) - .accessibilityFocused($isCardFocused) + GeometryReader { proxy in + ScrollView { + RegionWelcomeCard( + presentation: presentation, + dismissAction: dismissAction, + planStayAction: planStayAction, + ) + .fixedSize(horizontal: false, vertical: true) + .padding(.horizontal, stylesheet.spacing.xxxLarge) + .padding(.vertical, stylesheet.spacing.xxxLarge) + .frame(maxWidth: .infinity, minHeight: proxy.size.height) + .accessibilityFocused($isCardFocused) + } + .scrollBounceBehavior(.basedOnSize) + } + .contentShape(Rectangle()) + .accessibilityAddTraits(.isModal) + .transition(stylesheet.locationWelcome.motion.transition) + .zIndex(1) + .onAppear { + isCardFocused = true + UIAccessibility.post(notification: .screenChanged, argument: nil) + } + .onDisappear { + isCardFocused = false + UIAccessibility.post(notification: .screenChanged, argument: nil) } - .scrollBounceBehavior(.basedOnSize) } } - .contentShape(Rectangle()) - .accessibilityAddTraits(.isModal) - .onAppear { - isCardFocused = true - UIAccessibility.post(notification: .screenChanged, argument: nil) - } - .onDisappear { - UIAccessibility.post(notification: .screenChanged, argument: nil) - } + .animation(stylesheet.locationWelcome.motion.animation, value: presentation) } } diff --git a/Where/WhereUI/Sources/Primary/LocationsView.swift b/Where/WhereUI/Sources/Primary/LocationsView.swift index b5cb218bf..8041bc53b 100644 --- a/Where/WhereUI/Sources/Primary/LocationsView.swift +++ b/Where/WhereUI/Sources/Primary/LocationsView.swift @@ -112,16 +112,12 @@ struct LocationsView: View { } .accessibilityHidden(welcomePresentation != nil) .overlay { - if let presentation = welcomePresentation { - LocationWelcomeOverlay( - presentation: presentation, - dismissAction: welcome.dismiss, - planStayAction: welcomePlanStayAction, - ) - .transition(stylesheet.locationWelcome.motion.transition) - } + LocationWelcomeOverlay( + presentation: welcomePresentation, + dismissAction: welcome.dismiss, + planStayAction: welcomePlanStayAction, + ) } - .animation(stylesheet.locationWelcome.motion.animation, value: welcomePresentation) .task(id: isWelcomeLookupActive) { guard isWelcomeLookupActive else { return } await welcome.resolve() From e3d3e45ccd17c2975d10481319dc4e12b2cc07c3 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sun, 6 Sep 2026 19:42:26 -0700 Subject: [PATCH 2/3] feat(WhereUI): add a debug welcome-card reset in Appearance --- Where/WhereUI/AGENTS.md | 1 + Where/WhereUI/README.md | 3 ++ .../appearance.Default_iPad.png | 4 +-- .../appearance.Default_iPad_accessibility.png | 4 +-- .../appearance.Default_iPad_ax5.png | 4 +-- .../appearance.Default_iPad_contrast.png | 4 +-- .../appearance.Default_iPad_dark.png | 4 +-- .../appearance.Default_iPhone.png | 4 +-- ...ppearance.Default_iPhone_accessibility.png | 4 +-- .../appearance.Default_iPhone_ax5.png | 4 +-- .../appearance.Default_iPhone_contrast.png | 4 +-- .../appearance.Default_iPhone_dark.png | 4 +-- .../Sources/Model/YearReportModel.swift | 7 +++++ .../Sources/Resources/Localizable.xcstrings | 22 +++++++++++++++ .../Settings/AppearanceSettingsView.swift | 23 ++++++++++++++- .../Tests/LocationWelcomeModelTests.swift | 28 +++++++++++++++++++ 16 files changed, 103 insertions(+), 21 deletions(-) diff --git a/Where/WhereUI/AGENTS.md b/Where/WhereUI/AGENTS.md index 90d0b3154..d91de945f 100644 --- a/Where/WhereUI/AGENTS.md +++ b/Where/WhereUI/AGENTS.md @@ -158,6 +158,7 @@ worked examples. count animation and outline-cache behavior. - Keep the DEBUG Ranking Animation Lab session-only. It tunes the containing Location-card stack, never Card Designer persistence, exports, or app overrides. +- Keep the Appearance welcome reset under `#if DEBUG`. Clear only the saved welcome region through `YearReportModel.resetLocationWelcome()`. ## Testing diff --git a/Where/WhereUI/README.md b/Where/WhereUI/README.md index e2c5ce6d6..8eb9229ce 100644 --- a/Where/WhereUI/README.md +++ b/Where/WhereUI/README.md @@ -134,6 +134,9 @@ the feature [`Where/AGENTS.md`](../AGENTS.md) and this module's - **`RegionWelcomeCard`** — a centered Locations overlay that combines a region's emoji, icon, outline, Liquid Glass card treatment, and passport ink. It uses a spring transition and an opacity-only transition when Reduce Motion is enabled. + Debug builds include **Reset Welcome Card** in Settings > Appearance beside the welcome-card toggle. + The reset clears the saved region so the next Locations visit can show the card again. + Welcome cards must be enabled, and the device must resolve a tracked region with recording active. - **`OnboardingView` / `OnboardingFlowModel`** — the rendered first-run flow and its view-scoped observable coordinator, registered for the launch's diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad.png index 15ae7d909..9649b2de4 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8c404376e9f4021d86df9a49e287878ea43920e9145a6fcd0d5c070cf2b6953 -size 1299833 +oid sha256:14cf5e46b012c571c32deba5c3408bc993938b88145cc86b545b96012f97d4ae +size 1324002 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_accessibility.png index 87a8f7b0c..8ce6589d5 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f7a172148bd9ee8d74e220719127661c8dd20722a5ac39d3b814ab245644c4b -size 1306081 +oid sha256:af85c12e7cdf71936ea762931791822cdc5f9f2eb9db5e45fddc3145ac81a609 +size 1366718 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_ax5.png index 19bd1505d..da184262c 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13196d4dd04ea3a7b40a0b391b2757311580ed422853ffcd5e5fd01296145722 -size 3539335 +oid sha256:c3c8f0f121f3be6673eb5a8f55227c7c5162020a291c2425c3a0ae018f2cbefc +size 3677022 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_contrast.png index 0e6443421..67a48e44e 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2811b25e511d33474cfbb945e5f324d4c717087c0c95b0f66313da66ef840b4 -size 1363243 +oid sha256:16c2a03336a7b65f15fe3977d3989cf07c9310a873c964ad04c126ea09373bf3 +size 1386791 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_dark.png index cf539f78b..a0ebcdb0d 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3a72192d2c152ce44c254f40af253bf38b1b659ca88fea9c293351652e9abf7 -size 1541919 +oid sha256:1d8ca53335928a3fdc37e906cf49cbab3cd777ce10483a126124df4ece688bac +size 1567203 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone.png index 5dd12dadf..ea13e24a1 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67a17a855e40ee123106eb324a032517ebc547450d86b2628c7c960cad2d0611 -size 837739 +oid sha256:6add82f912f8f6ccbc09f758c5d4f4c9eeac859dde6f9dd69124515909511334 +size 871175 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_accessibility.png index e01c06e8b..ae342ea6a 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ab865fb220c599429ba1ea42badc20ec8f4cf47d03207af52c50e94caa9c7d5 -size 1024509 +oid sha256:b0b7853c270a051d0cdd57c6a7f314f04b0a0d40b3709dc5a9349e6175e2b40e +size 1099030 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_ax5.png index a00e187a6..498ed7cf4 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c09cc7eb5dbabc1120554df61ec356d233f0c2a531a0112799a27ac167916ef -size 2435957 +oid sha256:b451837539eed4f34eafbd00d6eade5a125210fbc4473cab4a79e1fd60874cc1 +size 2587694 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_contrast.png index 9a856d12a..699db7a38 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3d5091d586f5e1511afa22bfec29dcf3553458297c85f662286d09cadf05d26 -size 883379 +oid sha256:47610d1ad985c1b8847a66b50d2458fb99e34b75da017c65d97deb4adc0c1fe0 +size 917525 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_dark.png index 383c358e6..bd049a9d6 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd8c66be2db4a82f647556484dfb7c90a865addf7143c443c1870495805851c1 -size 927430 +oid sha256:5cf1c3d696d887dffc5ddbaf67c3f2e14787e385db63449893525a57989e2391 +size 962500 diff --git a/Where/WhereUI/Sources/Model/YearReportModel.swift b/Where/WhereUI/Sources/Model/YearReportModel.swift index 17eb77a26..569261ab1 100644 --- a/Where/WhereUI/Sources/Model/YearReportModel.swift +++ b/Where/WhereUI/Sources/Model/YearReportModel.swift @@ -196,6 +196,13 @@ public final class YearReportModel { } } + #if DEBUG + /// Clears the acknowledgement so the next visible Locations lookup can welcome again. + func resetLocationWelcome() { + preferences.lastWelcomedRegion = nil + } + #endif + /// GPS border-drift detection threshold (device setting). The setter persists /// it, forces a badge recount, and — through the observed mirror — re-keys /// `dataIssueScanInputs` so the Resolve list re-scans immediately, not just on diff --git a/Where/WhereUI/Sources/Resources/Localizable.xcstrings b/Where/WhereUI/Sources/Resources/Localizable.xcstrings index fb4a9878f..84b8fa563 100644 --- a/Where/WhereUI/Sources/Resources/Localizable.xcstrings +++ b/Where/WhereUI/Sources/Resources/Localizable.xcstrings @@ -6161,6 +6161,28 @@ } } }, + "settings.appearance.locationWelcome.reset.footer" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Developer: Clear the saved welcome region, then return to Locations to show the card again." + } + } + } + }, + "settings.appearance.locationWelcome.reset.title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reset Welcome Card" + } + } + } + }, "settings.appearance.locationWelcome.toggle" : { "comment" : "Toggle that controls whether live-region welcome cards appear on the Locations tab.", "extractionState" : "manual", diff --git a/Where/WhereUI/Sources/Settings/AppearanceSettingsView.swift b/Where/WhereUI/Sources/Settings/AppearanceSettingsView.swift index 5e082bdc7..7c6611df3 100644 --- a/Where/WhereUI/Sources/Settings/AppearanceSettingsView.swift +++ b/Where/WhereUI/Sources/Settings/AppearanceSettingsView.swift @@ -60,8 +60,24 @@ struct AppearanceSettingsView: View { ) } .settingsRow(Item.locationWelcome) + + #if DEBUG + Button(action: report.resetLocationWelcome) { + Label( + String(localized: .settingsAppearanceLocationWelcomeResetTitle), + systemSymbol: .arrowCounterclockwise, + ) + } + .disabled(!report.showsLocationWelcome) + .settingsRow(Item.resetLocationWelcome) + #endif } footer: { - Text(String(localized: .settingsAppearanceLocationWelcomeFooter)) + VStack(alignment: .leading) { + Text(String(localized: .settingsAppearanceLocationWelcomeFooter)) + #if DEBUG + Text(String(localized: .settingsAppearanceLocationWelcomeResetFooter)) + #endif + } } Section { @@ -157,6 +173,7 @@ extension AppearanceSettingsView: SettingsSection { case locationForecasts case appIcon #if DEBUG + case resetLocationWelcome case cardDesigner case rankingAnimation #endif @@ -172,6 +189,8 @@ extension AppearanceSettingsView: SettingsSection { String(localized: .settingsAppearanceLocationForecastsToggle) case .appIcon: String(localized: .settingsAppIconLink) #if DEBUG + case .resetLocationWelcome: + String(localized: .settingsAppearanceLocationWelcomeResetTitle) case .cardDesigner: String(localized: .cardDesignerTitle) case .rankingAnimation: String(localized: .rankingAnimationTitle) #endif @@ -190,6 +209,8 @@ extension AppearanceSettingsView: SettingsSection { splitKeywords(String(localized: .settingsKeywordsLocationForecasts)) case .appIcon: splitKeywords(String(localized: .settingsKeywordsAppIcon)) #if DEBUG + case .resetLocationWelcome: + splitKeywords(String(localized: .settingsKeywordsLocationWelcome)) case .cardDesigner: splitKeywords(String(localized: .cardDesignerSettingsKeywords)) case .rankingAnimation: diff --git a/Where/WhereUI/Tests/LocationWelcomeModelTests.swift b/Where/WhereUI/Tests/LocationWelcomeModelTests.swift index aabf3e855..f6b8176f5 100644 --- a/Where/WhereUI/Tests/LocationWelcomeModelTests.swift +++ b/Where/WhereUI/Tests/LocationWelcomeModelTests.swift @@ -39,6 +39,34 @@ struct LocationWelcomeModelTests { #expect(fixture.model.presentation == .init(region: .newYork, greeting: .returnVisit)) } + @Test(arguments: [true, false]) + func appearanceResetAllowsTheSameRegionToWelcomeAgainWhenEnabled(isEnabled: Bool) async throws { + let fixture = try await fixture(region: .california) + await fixture.model.resolve() + fixture.model.dismiss() + await fixture.model.resolve() + #expect(fixture.model.presentation == nil) + + let report = YearReportModel( + services: fixture.services, + selectedYear: 2026, + preferences: fixture.preferences, + ) + report.showsLocationWelcome = isEnabled + report.resetLocationWelcome() + + #expect(fixture.preferences.lastWelcomedRegion == nil) + #expect(fixture.preferences.showsLocationWelcome == isEnabled) + #expect(fixture.model.presentation == nil) + + await fixture.model.resolve() + + #expect(fixture.model.presentation == (isEnabled ? .init( + region: .california, + greeting: .first, + ) : nil)) + } + @Test func inactiveRecordingDoesNotPresent() async throws { let fixture = try fixtureWithoutRecording(region: .california) From c9de8c6244990202929c3953ef18f73831863d35 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sun, 6 Sep 2026 19:56:34 -0700 Subject: [PATCH 3/3] feat(WhereUI): stamp welcome cards in and lift them away --- Where/WhereUI/AGENTS.md | 1 + Where/WhereUI/README.md | 5 +- .../Primary/LocationWelcomeOverlay.swift | 2 +- .../LocationWelcomeTransitionModifier.swift | 15 ++++ .../Sources/Primary/LocationsView.swift | 2 +- .../Sources/Shared/WhereStylesheet.swift | 73 +++++++++++++++---- .../WhereUI/Tests/WhereStylesheetTests.swift | 22 ++++++ 7 files changed, 100 insertions(+), 20 deletions(-) create mode 100644 Where/WhereUI/Sources/Primary/LocationWelcomeTransitionModifier.swift diff --git a/Where/WhereUI/AGENTS.md b/Where/WhereUI/AGENTS.md index d91de945f..3d3454f65 100644 --- a/Where/WhereUI/AGENTS.md +++ b/Where/WhereUI/AGENTS.md @@ -159,6 +159,7 @@ worked examples. - Keep the DEBUG Ranking Animation Lab session-only. It tunes the containing Location-card stack, never Card Designer persistence, exports, or app overrides. - Keep the Appearance welcome reset under `#if DEBUG`. Clear only the saved welcome region through `YearReportModel.resetLocationWelcome()`. +- Keep welcome-card arrival, departure, and scrim timing in `locationWelcome.motion`. Apply spatial transitions only to the card layer. ## Testing diff --git a/Where/WhereUI/README.md b/Where/WhereUI/README.md index 8eb9229ce..6702d211b 100644 --- a/Where/WhereUI/README.md +++ b/Where/WhereUI/README.md @@ -132,8 +132,9 @@ the feature [`Where/AGENTS.md`](../AGENTS.md) and this module's ### Reusable views & styling - **`RegionWelcomeCard`** — a centered Locations overlay that combines a region's emoji, - icon, outline, Liquid Glass card treatment, and passport ink. It uses a spring transition - and an opacity-only transition when Reduce Motion is enabled. + icon, outline, Liquid Glass card treatment, and passport ink. The card stamps into place + with a quick tilted approach and spring settle, then lifts away on dismissal. + The scrim fades independently. Reduce Motion uses a short fade for both layers. Debug builds include **Reset Welcome Card** in Settings > Appearance beside the welcome-card toggle. The reset clears the saved region so the next Locations visit can show the card again. Welcome cards must be enabled, and the device must resolve a tracked region with recording active. diff --git a/Where/WhereUI/Sources/Primary/LocationWelcomeOverlay.swift b/Where/WhereUI/Sources/Primary/LocationWelcomeOverlay.swift index 3b12312bc..f8e78d350 100644 --- a/Where/WhereUI/Sources/Primary/LocationWelcomeOverlay.swift +++ b/Where/WhereUI/Sources/Primary/LocationWelcomeOverlay.swift @@ -51,7 +51,7 @@ struct LocationWelcomeOverlay: View { } } } - .animation(stylesheet.locationWelcome.motion.animation, value: presentation) + .animation(stylesheet.locationWelcome.motion.scrimAnimation, value: presentation) } } diff --git a/Where/WhereUI/Sources/Primary/LocationWelcomeTransitionModifier.swift b/Where/WhereUI/Sources/Primary/LocationWelcomeTransitionModifier.swift new file mode 100644 index 000000000..dcbffb673 --- /dev/null +++ b/Where/WhereUI/Sources/Primary/LocationWelcomeTransitionModifier.swift @@ -0,0 +1,15 @@ +import SwiftUI + +/// Moves the complete welcome card like a stamp approaching or lifting from paper. +struct LocationWelcomeTransitionModifier: ViewModifier { + let scale: CGFloat + let rotationDegrees: Double + let verticalOffset: CGFloat + + func body(content: Content) -> some View { + content + .scaleEffect(scale) + .rotationEffect(.degrees(rotationDegrees)) + .offset(y: verticalOffset) + } +} diff --git a/Where/WhereUI/Sources/Primary/LocationsView.swift b/Where/WhereUI/Sources/Primary/LocationsView.swift index 8041bc53b..7d0d3fa06 100644 --- a/Where/WhereUI/Sources/Primary/LocationsView.swift +++ b/Where/WhereUI/Sources/Primary/LocationsView.swift @@ -302,7 +302,7 @@ struct LocationsView: View { } private func planStayFromWelcome(_ region: Region) { - withAnimation(stylesheet.locationWelcome.motion.animation) { + withAnimation(stylesheet.locationWelcome.motion.departure.animation) { welcome.dismiss() } completion: { editPlannedStay(region) diff --git a/Where/WhereUI/Sources/Shared/WhereStylesheet.swift b/Where/WhereUI/Sources/Shared/WhereStylesheet.swift index 638eb218e..ceca2798e 100644 --- a/Where/WhereUI/Sources/Shared/WhereStylesheet.swift +++ b/Where/WhereUI/Sources/Shared/WhereStylesheet.swift @@ -131,33 +131,74 @@ extension WhereStylesheet { } struct Motion: Equatable { - var animation: Animation - var scale: CGFloat - var verticalOffset: CGFloat + var arrival: Movement + var departure: Movement + var scrimAnimation: Animation var usesSpatialMotion: Bool - var transition: AnyTransition { - let base: AnyTransition = usesSpatialMotion - ? .scale(scale: scale).combined(with: .offset(y: verticalOffset)) + struct Movement: Equatable { + var animation: Animation + var scale: CGFloat + var rotationDegrees: Double + var verticalOffset: CGFloat + + var transition: AnyTransition { + .modifier( + active: LocationWelcomeTransitionModifier( + scale: scale, + rotationDegrees: rotationDegrees, + verticalOffset: verticalOffset, + ), + identity: LocationWelcomeTransitionModifier( + scale: 1, + rotationDegrees: 0, + verticalOffset: 0, + ), + ) .combined(with: .opacity) - : .opacity - return .asymmetric( - insertion: base.animation(animation), - removal: base.animation(animation), + } + } + + var transition: AnyTransition { + .asymmetric( + insertion: (usesSpatialMotion ? arrival.transition : .opacity) + .animation(arrival.animation), + removal: (usesSpatialMotion ? departure.transition : .opacity) + .animation(departure.animation), ) } static let standard = Motion( - animation: .spring(duration: 0.62, bounce: 0.32), - scale: 0.78, - verticalOffset: 34, + arrival: Movement( + animation: .spring(duration: 0.3, bounce: 0.28), + scale: 1.28, + rotationDegrees: -9, + verticalOffset: -24, + ), + departure: Movement( + animation: .easeOut(duration: 0.16), + scale: 1.045, + rotationDegrees: 3, + verticalOffset: -10, + ), + scrimAnimation: .easeOut(duration: 0.16), usesSpatialMotion: true, ) static let reduced = Motion( - animation: .easeInOut(duration: 0.18), - scale: 1, - verticalOffset: 0, + arrival: Movement( + animation: .easeInOut(duration: 0.16), + scale: 1, + rotationDegrees: 0, + verticalOffset: 0, + ), + departure: Movement( + animation: .easeInOut(duration: 0.16), + scale: 1, + rotationDegrees: 0, + verticalOffset: 0, + ), + scrimAnimation: .easeInOut(duration: 0.16), usesSpatialMotion: false, ) } diff --git a/Where/WhereUI/Tests/WhereStylesheetTests.swift b/Where/WhereUI/Tests/WhereStylesheetTests.swift index 208297c6d..f903af7b4 100644 --- a/Where/WhereUI/Tests/WhereStylesheetTests.swift +++ b/Where/WhereUI/Tests/WhereStylesheetTests.swift @@ -61,6 +61,19 @@ struct WhereStylesheetTests { #expect(welcome.close.glow == .init(opacity: 0.28, radius: 8)) #expect(welcome.close.lift == .init(opacity: 0.22, radius: 5, offsetY: 3)) #expect(welcome.motion == .standard) + #expect(welcome.motion.arrival == .init( + animation: .spring(duration: 0.3, bounce: 0.28), + scale: 1.28, + rotationDegrees: -9, + verticalOffset: -24, + )) + #expect(welcome.motion.departure == .init( + animation: .easeOut(duration: 0.16), + scale: 1.045, + rotationDegrees: 3, + verticalOffset: -10, + )) + #expect(welcome.motion.scrimAnimation == .easeOut(duration: 0.16)) } @Test func regularCardStyle() { @@ -842,6 +855,15 @@ struct WhereStylesheetTests { #expect(resolved.locationCardStack.overtake.usesSpatialMotion == false) #expect(resolved.locationWelcome.motion == .reduced) #expect(resolved.locationWelcome.motion.usesSpatialMotion == false) + #expect(resolved.locationWelcome.motion.arrival == .init( + animation: .easeInOut(duration: 0.16), + scale: 1, + rotationDegrees: 0, + verticalOffset: 0, + )) + #expect(resolved.locationWelcome.motion.departure == resolved.locationWelcome.motion + .arrival) + #expect(resolved.locationWelcome.motion.scrimAnimation == .easeInOut(duration: 0.16)) #expect(resolved.developerOverlay.menu.motion == .reduced) #expect(resolved.developerOverlay.menu.motion.usesSpatialMotion == false) }