fix: ignore tap navigation while user is mid-swipe#747
Merged
everpcpc merged 1 commit intoeverpcpc:mainfrom Apr 29, 2026
Merged
fix: ignore tap navigation while user is mid-swipe#747everpcpc merged 1 commit intoeverpcpc:mainfrom
everpcpc merged 1 commit intoeverpcpc:mainfrom
Conversation
When a tap on the page edge arrived during an in-flight finger drag (or its deceleration / commit-cancel animation), the tap fired a second navigation that interrupted or layered onto the user's gesture. The net result was a double page advance — or, in the two-finger case, the tap overriding the drag entirely. Scroll mode: `handleNavigationChange` already guarded against `isProgrammaticScrolling` but not `isUserInteracting`. The engine's `isUserInteracting` flag spans drag start through deceleration end, so add an early return for that state and clear `navigationTarget` so the tap is fully discarded rather than queued for after deceleration. Cover mode: `update(...)` guarded `handleNavigationTarget` with `!isAnimatingTransition`, which only covers the post-pan commit/cancel animation — during the active pan itself the flag is false. Derive `isUserPanning` from `panRecognizer.state` (`.began` or `.changed`) and discard tap-initiated navigation while panning, mirroring the scroll behavior. Page Curl is unaffected; UIPageViewController already rejects competing transitions during an interactive transition. Both fixes are mirrors of guards already present in the same files for the in-flight programmatic state — they just close the symmetric gap where in-flight user interaction needed the same protection. Closes everpcpc#746 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Closes #746
Summary
When a tap on the page edge lands while a finger drag (or its deceleration / commit-cancel animation) is still in flight, the tap fires a second navigation on top of the in-flight transition — causing a double page advance, or in the two-finger case the tap overriding the drag entirely.
This affects Scroll and Cover transition styles. Page Curl is unaffected (UIPageViewController already rejects competing transitions during an interactive transition).
Fix: in both modes, gate tap-initiated navigation on the in-flight user-interaction state and discard the tap (clear
navigationTarget) when blocked, so the tap doesn't get queued for after the drag finishes.Root cause
Scroll (
ScrollPageView_iOS.swift/ScrollPageView_macOS.swift)handleNavigationChangealready guards against!hasSyncedInitialPositionandisProgrammaticScrolling && isPendingProgrammaticCommit(target), but notengine.isUserInteracting. The engine sets that flag atscrollViewWillBeginDraggingand clears it atscrollViewDidEndDecelerating— i.e., it spans the full drag-plus-deceleration window. A tap arriving in that window propagated tohandleNavigationChange, which calledscrollToItem(animated: true)→setContentOffset(target, animated: true), interrupting the natural deceleration with a new programmatic scroll.There is already a symmetric guard going the other direction (
scrollViewWillBeginDragging→cancelProgrammaticNavigationIfNeeded) that lets a fresh drag override an in-flight programmatic scroll. The reverse — in-flight drag overrides a new programmatic scroll — is what was missing.Cover (
NativeCoverPageView_iOS.swift/NativeCoverPageView_macOS.swift)update(...)guardedhandleNavigationTargetwith!isAnimatingTransition, which only covers the post-pan commit/cancel animation. During the active pan itself (UIPanGestureRecognizer.state == .changed),isAnimatingTransitionisfalse. A tap during the active pan therefore passed the guard and firedhandleNavigationTarget→commitTransition(to:, animation: .tapNavigation), interrupting the user's drag with a programmatic transition.Fix
Scroll — early return in
handleNavigationChange:Cover — derive
isUserPanningfrom the recognizer state and gate the navigation branch:clearNavigationTarget()ensures the tap is discarded, not queued. Without it, the tap would sit as a pending navigation target and fire as soon as the drag completed — producing a delayed page jump that's the same bug felt slightly differently.Why this is structurally correct
Both fixes are mirrors of guards already present in the same files for the in-flight programmatic state. The pattern in each file is "do not start a new navigation while another navigation is in flight." Each file only checked half of that — the programmatic half. These changes close the symmetric gap by also checking the user-interaction half.
Scope
ScrollPageView_iOS.swifthandleNavigationChangeScrollPageView_macOS.swiftNativeCoverPageView_iOS.swiftisUserPanningproperty + guard inupdateNativeCoverPageView_macOS.swiftPageCurl untouched; reporter confirmed it's already protected by UIKit.
Testing
Not tested. I do not have an Xcode build environment for this project, so the change has not been built or run. The reasoning is purely static:
engine.isUserInteractingis already a known-good signal —cancelProgrammaticNavigationIfNeeded(line 506 iOS) reads similar state and is invoked from the same lifecycle (drag start). The guard simply uses it in the opposite direction.panRecognizer.stateis the standard UIKit/AppKit mechanism for querying recognizer state at any time;.beganand.changedare exactly the active-touch states.applyPanRecognizerState(line 208 iOS / equivalent macOS) already inspectspreviousState == .began || previousState == .changed, so this pattern is already used in the same file.clearNavigationTargetis the same call already used at line 305 / 314 / 321 / 326 / 342 ofNativeCoverPageView_iOSfor unresolved or no-op targets.A maintainer with the build environment should verify:
isAnimatingTransitionguard, unchanged).🤖 Generated with Claude Code