Skip to content

fix(ios): drive gesture pinch via two-finger synthesis (#629)#634

Merged
thymikee merged 1 commit into
mainfrom
perf/ios-pinch-synthesis
May 31, 2026
Merged

fix(ios): drive gesture pinch via two-finger synthesis (#629)#634
thymikee merged 1 commit into
mainfrom
perf/ios-pinch-synthesis

Conversation

@thymikee
Copy link
Copy Markdown
Member

What

gesture pinch on iOS now uses the two-finger XCTest synthesis path instead of a single-finger coordinate drag, so React Native's pinch recognizer actually fires.

Root cause (#629)

On iOS the runner lowered pinch to performCoordinatePinch — a tap() followed by a single-finger press(forDuration:thenDragTo:). React Native reads that as a pan: the target translates but the pinch scale never changes. Reproduced on iPhone 17 Pro against examples/test-app's gesture lab — "pinch changed no", scale 1.00.

Fix

On iOS, pinch() delegates to transformGesture(… dx:0, dy:0, degrees:0) — the same private two-finger synthesis (RunnerSynthesizedGesture) that gesture transform already uses and that does change scale. macOS keeps the coordinate path; tvOS stays unsupported. 20-line Swift diff, no TS changes.

Validation (iPhone 17 Pro, modified runner built)

  • Focused pinch replay: wait "pinch changed yes" now passes (failed before the fix).
  • Full examples/test-app/replays/gesture-lab.ad oracle: 1/1 pass (fling ×4 / pan ×4 / pinch / rotate). It previously failed at the pinch step. No regression to the other gestures.

Scope note

This is the one #629 gesture bug that reproduced on the current examples/test-app. The reported scroll/fling "miss" did not reproduce here (scroll/fling/pan register fine at baseline), so no gesture-duration changes are included.

Refs #629

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 31, 2026

Size Report

Metric Base Current Diff
JS raw 1.1 MB 1.1 MB 0 B
JS gzip 355.9 kB 355.9 kB 0 B
npm tarball 452.9 kB 453.1 kB +221 B
npm unpacked 1.5 MB 1.5 MB +633 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 25.6 ms 26.0 ms +0.3 ms
CLI --help 40.6 ms 40.7 ms +0.2 ms

Top changed chunks: no changes in the largest emitted chunks.

…gle-finger drag

On iOS the runner lowered `pinch` to performCoordinatePinch — a tap() then a
single-finger press(forDuration:thenDragTo:). React Native reads that as a pan,
so the pinch scale never changes (reported in #629: scale stays 1.00).

Route iOS pinch through the existing two-finger XCTest synthesis path
(transformGesture / RunnerSynthesizedGesture) with zero translation and rotation,
so RN's pinch recognizer fires. macOS keeps the coordinate path.

Validated on iPhone 17 Pro against examples/test-app: the gesture-lab.ad oracle
now passes 1/1 (fling/pan/pinch/rotate); it previously failed at the pinch step.

Refs #629
@thymikee thymikee force-pushed the perf/ios-pinch-synthesis branch from be86319 to 1fc88db Compare May 31, 2026 12:39
@thymikee thymikee merged commit fa4e2d5 into main May 31, 2026
18 checks passed
@thymikee thymikee deleted the perf/ios-pinch-synthesis branch May 31, 2026 13:09
@github-actions
Copy link
Copy Markdown

PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-05-31 13:09 UTC

thymikee added a commit that referenced this pull request Jun 1, 2026
… unsupported errors

Make RunnerSynthesizedGesture the single iOS multi-touch engine and drop the older
incompatible models:

- rotateGesture now drives the two-finger XCTest synthesis path (dx=dy=0, scale=1,
  degrees), mirroring the pinch migration in #634. The native XCUIElement.rotate(withVelocity:)
  injected a single synthetic rotation that React Native's rotation recognizer did not read
  reliably; synthesis fixes it. velocity is ignored on iOS (kept in the wire contract for
  compatibility; rotation direction comes from the sign of degrees).
- pinch is now synthesis on iOS and a clear UNSUPPORTED_OPERATION on tvOS/macOS. The macOS
  coordinate double-tap+drag heuristic (performCoordinatePinch) is removed: synthesis is
  iOS-only, so macOS multi-touch is reported honestly as unsupported rather than approximated.
- RunnerInteractionOutcome.unsupported now carries an actionable hint, mapped to ErrorPayload.hint
  (#639). Every unsupported gesture/tvOS path returns a concise message plus a next-step hint
  (existing messages kept verbatim).

Net: on iOS, pinch/rotate/transform all flow through one synthesis primitive. swipe/scroll/pan/
fling remain single-finger drags (correct, unchanged).

Coverage: examples/test-app/replays/gesture-lab.ad exercises pinch + rotate against the gesture
lab and asserts "pinch changed yes" / "rotate changed yes".
thymikee added a commit that referenced this pull request Jun 1, 2026
… unsupported errors (#645)

* fix(ios): unify multi-touch gestures on two-finger synthesis + hinted unsupported errors

Make RunnerSynthesizedGesture the single iOS multi-touch engine and drop the older
incompatible models:

- rotateGesture now drives the two-finger XCTest synthesis path (dx=dy=0, scale=1,
  degrees), mirroring the pinch migration in #634. The native XCUIElement.rotate(withVelocity:)
  injected a single synthetic rotation that React Native's rotation recognizer did not read
  reliably; synthesis fixes it. velocity is ignored on iOS (kept in the wire contract for
  compatibility; rotation direction comes from the sign of degrees).
- pinch is now synthesis on iOS and a clear UNSUPPORTED_OPERATION on tvOS/macOS. The macOS
  coordinate double-tap+drag heuristic (performCoordinatePinch) is removed: synthesis is
  iOS-only, so macOS multi-touch is reported honestly as unsupported rather than approximated.
- RunnerInteractionOutcome.unsupported now carries an actionable hint, mapped to ErrorPayload.hint
  (#639). Every unsupported gesture/tvOS path returns a concise message plus a next-step hint
  (existing messages kept verbatim).

Net: on iOS, pinch/rotate/transform all flow through one synthesis primitive. swipe/scroll/pan/
fling remain single-finger drags (correct, unchanged).

Coverage: examples/test-app/replays/gesture-lab.ad exercises pinch + rotate against the gesture
lab and asserts "pinch changed yes" / "rotate changed yes".

* fix(ios): fail-fast macOS pinch + align capability/docs with synthesis-only

Follow-through for removing the macOS coordinate pinch path (the runner now returns
UNSUPPORTED_OPERATION for macOS pinch): reject it at admission instead of round-tripping.

- capabilities.ts: pinch now matches rotate-gesture/transform-gesture (Android + iOS
  simulator only); macOS dropped. Removes the now-unused isMacOsOrMobileAppleSimulator helper.
- capabilities.test.ts: pinch expected unsupported on macOS and tvOS.
- website/docs/docs/commands.md: pinch listed for Android + iOS simulators only (removed from
  the macOS app-session list); documents that iOS rotate ignores the optional velocity arg
  (synthesis uses a fixed duration; direction comes from the sign of degrees).

Addresses PR #645 review HIGH #2 and MEDIUM #3.

* fix(ios): surface a hinted unsupported error for synthesis gestures at admission

Removing macOS pinch from the capability matrix makes macOS pinch (and the already-excluded
rotate-gesture/transform-gesture on macOS/tvOS/physical iOS) fail fast in ensureGenericCommandReady
before reaching the runner. That left the runner's macOS-specific hint unreachable on the daemon
path, so callers only saw the generic "<cmd> is not supported on this device".

Add an optional unsupportedHint to the capability matrix and surface it at admission, so the
synthesis-only gestures fail fast (no runner round-trip) AND return an actionable hint pointing to
where they work (Android + iOS simulator). Applied to pinch / rotate-gesture / transform-gesture.

Addresses PR #645 review (P2: route macOS pinch to the hinted failure).
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.

1 participant