From 0b90ddc0e0eb1f4e6de4d0bb36ecc06cc04c66fc Mon Sep 17 00:00:00 2001 From: Andre Emiliano Date: Mon, 21 Sep 2026 16:41:15 +0100 Subject: [PATCH] today: swipe the day the same way on both platforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #2378. The Today day-navigation swipe ran in opposite directions on Apple and Android. Apple stepped OLDER on a leftward drag in both shells; the Kotlin twin steps older on a RIGHTWARD one (`dayNavSwipeTarget`, pinned by `DayNavTest`), so the same gesture moved the day backwards on one platform and forwards on the other. The Kotlin comment beside that lane describes it as mirroring the iOS swipe, which is where the two look to have drifted apart. Apple moves to Android's direction rather than the reverse, for a reason beyond picking one: rightward-is-older is direct manipulation — dragging the content left brings the page to its right, the later day, into view — which is how a horizontally paged date view behaves elsewhere on the platform. The report behind the issue was exactly that expectation: a swipe right-to-left was expected to move forwards and went back instead. The direction now lives in one pure `TodayView.daySwipeDelta(dx:)` used by both Apple shells, rather than as a sign test written twice inside gesture closures. That is what made the drift invisible: the clamp either side of it was already pure and tested (`clampedDayOffset`, `TodayDayNavClampTests`), while the direction itself was not covered on this platform at all. Android's twin was already pure and already pinned, which is why its direction held. Behaviour otherwise unchanged: the same threshold and horizontal-dominance gates, the same clamp, the same chart-owns-its-own-touches mask on the classic shell. VERIFICATION xcodebuild -scheme Strand -destination 'platform=macOS' test \ -only-testing:StrandTests/TodayDayNavClampTests -> 17 tests, 0 failures xcodebuild -scheme NOOPiOS -configuration Debug \ -destination 'generic/platform=iOS' build -> BUILD SUCCEEDED cd android && ./gradlew testFullDebugUnitTest --tests "com.noop.ui.DayNavTest" -> 7 tests, 0 failures The three new Swift cases mirror the Kotlin ones by name and intent, so the two platforms now assert the same direction rather than describing it in prose. No Kotlin changes: Android already has the direction this adopts, and its test already pins it. One gradle invocation reported FAILED while its own test report recorded 7 passes and no failures, and it did not reproduce on a clean re-run; flagged rather than explained, since nothing in this change touches Kotlin. Not verified on a device: this is a gesture, and the branch has not been run on hardware. The direction is pinned by test on both platforms, but a device pass is the honest missing piece. Co-Authored-By: Claude Opus 5 --- Strand/Liquid/LiquidTodayView.swift | 5 +++-- Strand/Screens/TodayView.swift | 18 +++++++++++++++-- StrandTests/TodayDayNavClampTests.swift | 26 +++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/Strand/Liquid/LiquidTodayView.swift b/Strand/Liquid/LiquidTodayView.swift index 4791434a91..8532eb16a9 100644 --- a/Strand/Liquid/LiquidTodayView.swift +++ b/Strand/Liquid/LiquidTodayView.swift @@ -265,13 +265,14 @@ struct LiquidTodayView: View { } ) } - /// Horizontal swipe between days (left = older, right = newer), clamped to [today, earliest]. + /// Horizontal swipe between days (right = older, left = newer — `TodayView.daySwipeDelta`, #2378), + /// clamped to [today, earliest]. private var daySwipeGesture: some Gesture { DragGesture(minimumDistance: 24) .onEnded { value in let dx = value.translation.width, dy = value.translation.height guard abs(dx) > abs(dy) * 1.5, abs(dx) > 50 else { return } - let delta = dx < 0 ? 1 : -1 + let delta = TodayView.daySwipeDelta(dx: dx) let next = Self.clampedDayOffset(current: selectedDayOffset, delta: delta, maxOffset: earliestDayOffset) guard next != selectedDayOffset else { return } diff --git a/Strand/Screens/TodayView.swift b/Strand/Screens/TodayView.swift index eccee5c77c..0fd6604e49 100644 --- a/Strand/Screens/TodayView.swift +++ b/Strand/Screens/TodayView.swift @@ -705,6 +705,20 @@ struct TodayView: View { return min(upper, max(0, current + delta)) } + /// #2378 - the day step a horizontal swipe of `dx` points asks for: +1 OLDER, -1 NEWER. + /// + /// Rightward (dx > 0) is OLDER and leftward is NEWER, which is direct manipulation — dragging the + /// content left brings the page to its right, the later day, into view — and the direction the + /// Kotlin twin already takes (`dayNavSwipeTarget`, pinned by `DayNavTest`). Apple ran the opposite + /// way in both shells, so the same gesture moved the day backwards here and forwards there. + /// + /// Pure and shared by both Apple shells so the direction is pinned by a test rather than living + /// twice inside gesture closures, which is how the two platforms drifted apart unnoticed. + /// Mirror EXACTLY in Kotlin. + static func daySwipeDelta(dx: CGFloat) -> Int { + dx > 0 ? 1 : -1 + } + /// #16 - whole days-back offset for a date chosen in the day-nav picker, measured from the LOGICAL day /// (not raw Date()). Pure + unit-testable so the 00:00-04:00 rollover case is locked: in that window the /// logical day is the PREVIOUS calendar day, so anchoring the offset here (rather than on raw Date()) @@ -1251,8 +1265,8 @@ struct TodayView: View { let dy = value.translation.height // Horizontal-dominant and far enough to count as a deliberate day flip. guard abs(dx) > abs(dy) * 1.5, abs(dx) > 50 else { return } - // Swipe LEFT (dx < 0) -> OLDER day (+1 offset); swipe RIGHT -> NEWER day (-1 offset). - let delta = dx < 0 ? 1 : -1 + // Swipe RIGHT (dx > 0) -> OLDER day (+1 offset); swipe LEFT -> NEWER day (-1 offset). + let delta = Self.daySwipeDelta(dx: dx) let next = Self.clampedDayOffset(current: selectedDayOffset, delta: delta, maxOffset: earliestDayOffset) guard next != selectedDayOffset else { return } diff --git a/StrandTests/TodayDayNavClampTests.swift b/StrandTests/TodayDayNavClampTests.swift index 61270e8cc8..e6f3d80e48 100644 --- a/StrandTests/TodayDayNavClampTests.swift +++ b/StrandTests/TodayDayNavClampTests.swift @@ -93,4 +93,30 @@ final class TodayDayNavClampTests: XCTestCase { let picked = cal.date(from: DateComponents(year: 2026, month: 6, day: 24))! XCTAssertEqual(TodayView.pickedDayOffset(pickedDate: picked, anchorLogicalDay: logical), 4) } + + // MARK: - daySwipeDelta (#2378) + + /// A rightward drag reveals the past, so it steps OLDER (+1). Twin of the Kotlin + /// `DayNavTest.rightwardSwipeGoesOlder`, which pins the same direction for `dayNavSwipeTarget`. + func testRightwardSwipeStepsOlder() { + XCTAssertEqual(TodayView.daySwipeDelta(dx: 120), 1) + XCTAssertEqual(TodayView.daySwipeDelta(dx: 51), 1) + } + + /// A leftward drag brings the later day in from the right, so it steps NEWER (-1). Twin of the + /// Kotlin `DayNavTest.leftwardSwipeGoesNewerClampedAtToday` (the clamp itself is covered above). + func testLeftwardSwipeStepsNewer() { + XCTAssertEqual(TodayView.daySwipeDelta(dx: -120), -1) + XCTAssertEqual(TodayView.daySwipeDelta(dx: -51), -1) + } + + /// Composed with the clamp: from today a leftward swipe cannot reach a future day, and a rightward + /// one reaches yesterday — the two halves the gesture closures actually chain. + func testSwipeDeltaComposedWithClamp() { + let older = TodayView.daySwipeDelta(dx: 200) + XCTAssertEqual(TodayView.clampedDayOffset(current: 0, delta: older, maxOffset: 30), 1) + let newer = TodayView.daySwipeDelta(dx: -200) + XCTAssertEqual(TodayView.clampedDayOffset(current: 0, delta: newer, maxOffset: 30), 0) + XCTAssertEqual(TodayView.clampedDayOffset(current: 5, delta: newer, maxOffset: 30), 4) + } }