fix(swift): stop OptimizedEntry tap tracking from blocking nested Buttons [NT-3829] - #411
Merged
Merged
Conversation
…tons SwiftUI's .simultaneousGesture(TapGesture()), despite being documented to coexist with nested gestures, has version-dependent quirks where it still wins the gesture arena against a Button's own tap machinery and swallows the button's action when trackTaps is enabled. Replace it with a plain UITapGestureRecognizer attached to the shared ancestor view (cancelsTouchesInView = false, delegate always allows simultaneous recognition), which observes taps without ever competing for touches a nested interactive child needs. Falls back to the original SwiftUI gesture on non-UIKit platforms (e.g. macOS).
…829] Adds a reference screen (SwiftUI + UIKit) with a Button nested inside an OptimizedEntry/OptimizedEntryUIView with trackTaps: true, plus an XCUITest that taps the nested button and asserts its own action fires. Verified manually against both app shells: the SwiftUI scheme confirms the TapTrackingModifier fix; the UIKit scheme also passes, since OptimizedEntryUIView's UITapGestureRecognizer does not exhibit the same blocking behavior against a UIButton that SwiftUI's .simultaneousGesture had against a SwiftUI Button.
…[NT-3829] Comment-only: explains why the new "Nested Button Tap Test" button was added to MainScreen/MainViewController, flags the trackTaps: true line in each test screen as the exact setting from the bug report, and adds step-by-step comments to the XCUITest explaining what each step verifies.
David Nalchevanidze (nalchevanidze)
marked this pull request as ready for review
August 4, 2026 13:01
Charles Hudson (phobetron)
approved these changes
Aug 4, 2026
Charles Hudson (phobetron)
left a comment
Collaborator
There was a problem hiding this comment.
That's an interesting issue, and a great catch!
Felipe Mamud (fmamud)
approved these changes
Aug 4, 2026
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.
NT-3829
Summary
Tapping a Book button nested inside an
OptimizedEntrystopped working whenever that entry hadtrackTaps: true. The button sat underneath the entry's tap-tracking gesture, and that outer gesture was swallowing the touch before the button's own action ever fired.The root cause was SwiftUI's
.simultaneousGesture(TapGesture()): it's documented to coexist peacefully with a nested interactive child, but in practice SwiftUI's gesture-arena composition against aButton's own tap machinery is unreliable across iOS versions, and here it was winning the arena and blocking the button.The fix replaces that SwiftUI-managed gesture with an explicit
UITapGestureRecognizer, installed on the shared ancestor view withcancelsTouchesInView = falseand a delegate that unconditionally allows simultaneous recognition. Those are deterministic UIKit-level guarantees rather than SwiftUI's internal, version-dependent bridging, so the tracking gesture can observe every tap without ever having the option to consume or block a nested child's own touches.This package targets both iOS and macOS (see
Package.swift).UIKitexists on iOS but not on plain macOS, so the fix is guarded behind#if canImport(UIKit): every iPhone (and iPad) gets the fixedUITapGestureRecognizerbehavior described above; only the macOS target keeps the old SwiftUI.simultaneousGesturepath, since that's the only platform this package ships to where UIKit isn't available.Real-world verification
Reproduced and confirmed manually against the Travel Guide app in contentful/personalization-website-agent-benchmarks, built locally against this branch: the Book button nested inside an
OptimizedEntrywithtrackTaps: truewas untappable before this fix and responded correctly after it.New regression coverage
Added a reference screen (SwiftUI + UIKit) with a
Button/UIButtonnested inside anOptimizedEntry/OptimizedEntryUIViewwithtrackTaps: true(wired intoMainScreen.swift/MainViewController.swiftbehind a new "Nested Button Tap Test" button, matching how the existing Navigation/Live Updates test screens are reached), plus an E2E XCUITest (testTappingNestedButtonWithTrackTapsEnabled) that taps the nested button and asserts its own action fires..simultaneousGesture) is exercised by the package's existing unit tests (swift build+swift teston the macOS host target) — this package has no macOS app target, so there's no macOS E2E run.OptimizationAppSwiftUIconfirms theTapTrackingModifierfix directly.OptimizationAppUIKitalso passes —OptimizedEntryUIView'sUITapGestureRecognizerdoesn't exhibit the same blocking behavior against aUIButtonthat SwiftUI's.simultaneousGesturehad against a SwiftUIButton, so no equivalent UIKit-side fix was needed.Why both
swiftui/Screensanduikit/Screenschanged for one testimplementations/ios-sdk/uitests/Tests/TapTrackingTests.swiftis shared source: perproject.yml, both theOptimizationAppUITestsSwiftUIandOptimizationAppUITestsUIKittargets listuitestsas a source directory, so this one test file is compiled twice — once into each test bundle — and run against two different real apps.testTappingNestedButtonWithTrackTapsEnableddoesn't know or care which app it's driving; it just taps elements by accessibility identifier (nested-button-tap-test-button,book-button,book-tap-count). That's why this PR adds two screens with matching identifiers —NestedButtonTapTestScreen.swift(SwiftUI) andNestedButtonTapTestViewController.swift(UIKit) — rather than one: they're the two concrete implementations the single shared test drives, one per app shell.There is no third iOS variant "without UIKit" to also cover — UIKit isn't optional on iOS/iPadOS; SwiftUI itself is built on top of it there. The only platform in this package without UIKit is plain macOS, and this repo has no macOS reference app or E2E test target at all, which is why the macOS fallback branch is covered by the package's unit tests only (see above), not an E2E run.
Test plan
swift build(macOS host target — exercises the macOS fallback branch)xcodebuild -scheme ContentfulOptimization -destination 'platform=iOS Simulator,name=iPhone 17' build(exercises the fixed UIKit branch)swift test— all 179 existing unit tests pass unmodifiedpersonalization-website-agent-benchmarks, built locally against this branchTapTrackingTests/testTappingNestedButtonWithTrackTapsEnabledon iPhone Simulator — passes on bothOptimizationAppSwiftUIandOptimizationAppUIKitschemes🤖 Generated with Claude Code