Skip to content

fix(swift): stop OptimizedEntry tap tracking from blocking nested Buttons [NT-3829] - #411

Merged
David Nalchevanidze (nalchevanidze) merged 5 commits into
mainfrom
NT-3829
Aug 4, 2026
Merged

fix(swift): stop OptimizedEntry tap tracking from blocking nested Buttons [NT-3829]#411
David Nalchevanidze (nalchevanidze) merged 5 commits into
mainfrom
NT-3829

Conversation

@nalchevanidze

@nalchevanidze David Nalchevanidze (nalchevanidze) commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

NT-3829

Summary

Tapping a Book button nested inside an OptimizedEntry stopped working whenever that entry had trackTaps: 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 a Button'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 with cancelsTouchesInView = false and 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). UIKit exists on iOS but not on plain macOS, so the fix is guarded behind #if canImport(UIKit): every iPhone (and iPad) gets the fixed UITapGestureRecognizer behavior described above; only the macOS target keeps the old SwiftUI .simultaneousGesture path, 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 OptimizedEntry with trackTaps: true was untappable before this fix and responded correctly after it.

New regression coverage

Added a reference screen (SwiftUI + UIKit) with a Button/UIButton nested inside an OptimizedEntry/OptimizedEntryUIView with trackTaps: true (wired into MainScreen.swift/MainViewController.swift behind 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.

  • The macOS fallback path (.simultaneousGesture) is exercised by the package's existing unit tests (swift build + swift test on the macOS host target) — this package has no macOS app target, so there's no macOS E2E run.
  • The new E2E test runs on iPhone Simulator against both app shells and passes on each:
    • OptimizationAppSwiftUI confirms the TapTrackingModifier fix directly.
    • OptimizationAppUIKit also passes — OptimizedEntryUIView's UITapGestureRecognizer doesn't exhibit the same blocking behavior against a UIButton that SwiftUI's .simultaneousGesture had against a SwiftUI Button, so no equivalent UIKit-side fix was needed.

Why both swiftui/Screens and uikit/Screens changed for one test

implementations/ios-sdk/uitests/Tests/TapTrackingTests.swift is shared source: per project.yml, both the OptimizationAppUITestsSwiftUI and OptimizationAppUITestsUIKit targets list uitests as a source directory, so this one test file is compiled twice — once into each test bundle — and run against two different real apps. testTappingNestedButtonWithTrackTapsEnabled doesn'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) and NestedButtonTapTestViewController.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 unmodified
  • Manual verification against the Travel Guide app in personalization-website-agent-benchmarks, built locally against this branch
  • New E2E test TapTrackingTests/testTappingNestedButtonWithTrackTapsEnabled on iPhone Simulator — passes on both OptimizationAppSwiftUI and OptimizationAppUIKit schemes

🤖 Generated with Claude Code

…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).
@nalchevanidze David Nalchevanidze (nalchevanidze) changed the title fix(swift): stop OptimizedEntry tap tracking from blocking nested Buttons fix(swift): stop OptimizedEntry tap tracking from blocking nested Buttons [NT-3829] Aug 4, 2026
…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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's an interesting issue, and a great catch!

@nalchevanidze
David Nalchevanidze (nalchevanidze) merged commit 8c29c1c into main Aug 4, 2026
44 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants