Skip to content

fix(ios): deliver simulator edge swipes as screen-edge touches - #4211

Merged
thymikee merged 12 commits into
callstack:mainfrom
gronxb:fix/ios-simulator-edge-swipe
Nov 5, 2026
Merged

thymikee merged 12 commits into
callstack:mainfrom
gronxb:fix/ios-simulator-edge-swipe

Conversation

@gronxb

@gronxb gronxb commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

On iOS simulators, gesture swipe left-edge/right-edge never started edge recognizers or the navigation edge pop: XCTest touches carry no edge information, even at x = 0.

Edge presets now plan a screen-edge contact (contactOrigin). On an iOS simulator, the runner sends that plan as one HID contact the way a hand swipe arrives: SwipeLocked plus the native swipe direction on every frame, through the oldest touchscreen service of the app's display (the lit panel on a foldable). The contact eases in along the planned path, because a first move that leaves the edge region in one frame fails the recognizer on a wide panel. Edge and sampled gestures resolve their surface and native samples through one helper on the surface geometry from #4189.

  • Terminal event: the contact owner builds a cancel event before touch-down. After touch-down, every exit sends the touch-up or that cancel, stamped at dispatch. A cancel that cannot be built stops the command before anything is sent. The contact owner lives in a header whose declarations all have internal linkage; the fault-injection test facade lives in UnitTests/, which the packager drops.
  • Touchscreen selection: a candidate is a touchscreen (usage page 13, usage 4) on the app's display with a readable registry ID. The oldest CoreDevice one wins, as before. A touchscreen has Transport=CoreDevice only while Xcode's Device Hub shows the simulator, through the simulator's dtuhidd. A headless simulator, or one without Device Hub, has only the SimulatorHID touchscreen, which names no Transport (reported on iOS 18.1 and 26.5); without a CoreDevice candidate, the display's sole touchscreen with a missing Transport is used. Several such touchscreens are ambiguous, and a Transport of another value or type refuses, so an unexpected sender is never picked. Errors name the stage that rejected the touchscreens. The runner logs the chosen service and its properties as AGENT_DEVICE_RUNNER_EDGE_TOUCHSCREEN.
  • Reported schedule: the runner execution policy resolves the contact and its schedule together and returns executionProfile (edge-ease-in for this path). gestureResponseData keeps any schedule the backend reports and uses the planned profile only when a backend reports none. ADR 0013 records edge-ease-in and that rule. Plans still cannot request it.

Other gestures and physical devices keep their paths. 23 files. Fixes #4208.

Validation

Rebased onto 66438647e (#4189 replaced the rotation frame with surface geometry). At 3b94f2eae:

  • pnpm check:affected --run --base 66438647e --head 3b94f2eae: passed, exactCommit=true. pnpm check:package: passed.
  • Runner unit tests (iOS 27.2): edge contact, touchscreen selection and planned gesture tests pass. testAlertActivationConfirmsBeforeDeadlineWithoutHandlingTheNotificationBanner and testFreshCoordinateTapContainsUnavailableTextInputProbe fail on 66438647e too.
  • Headless ./repro.sh: iPhone 16e iOS 26.4, iPhone 16e iOS 27.2, iPhone Duo iOS 27.1: 5/5 each, every edge touch through the touchscreen without Transport.

Before the rebase, at 8a57d06ae, base 035c2b6ca. The repro tables below ran at 8a57d06ae, except iPhone 16 (9544fa2cf; it was taken by another session), replaced by iPhone 17 Pro on iOS 26.4.

  • pnpm check:package: passed, no test code in the packed files.
  • pnpm check:affected --run --head 8a57d06ae: passed, exactCommit=true.
  • Swift unit tests (AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS=1, iOS 26.4) include HID constructor failures at each frame and touchscreen selection over fake services. Without the cancel dispatch, the move/lift failure test fails. The reported service (no Transport) failed selection with display 1 has no simulator touchscreen HID service before 9544fa2cf.
  • Repro ./repro.sh, three edge cases plus in-page swipe and pan controls:
Simulator Base Head
iPhone 16, iOS 26.4 0/3 edge 5/5 (9544fa2cf)
iPhone 17 Pro, iOS 26.4 — 5/5
iPhone 18 Pro, iOS 27.2 — 5/5
iPhone 16, iOS 26.5 (23F73) — 5/5
iPhone Duo, iOS 27.1, open 0/3 edge 5/5
iPhone Duo, iOS 27.1, closed — 5/5

Headless (simctl boot, no Device Hub), every touchscreen without Transport:

Simulator Before 9544fa2cf (7a39ef071) 8a57d06ae
iPhone 16e, iOS 26.4 display 1 has no simulator touchscreen HID service 5/5
iPhone 16e, iOS 27.2 (2 touchscreens) — 5/5
iPhone Duo, iOS 27.1 (3 touchscreens) — 5/5
  • The repro prints the touchscreen HID services and the service each edge touch used. With Device Hub, every run selected the oldest CoreDevice touchscreen of the app's display. Headless, every run selected the display's sole touchscreen without Transport. A headless scan of iOS 26.4, 26.5, 27.0, 27.1 and 27.2 (iPhone, iPad, Duo) found no Transport on any touchscreen.
  • --json timing: right-edge → edge-ease-in, swipe right → endpoint-hold, pan → timed-pan.
  • An injected cancel delivers touchesCancelled, and the next touch begins normally.

Open risks: physical devices are unchanged and untested. On the first run right after a fold, the left-edge recognizer case failed once on the Duo: the touchscreen it selected was not yet in the service list printed seconds earlier, so the fold was still recreating the display's HID services. The next two runs passed 5/5.

@vercel

vercel Bot commented Nov 4, 2026

Copy link
Copy Markdown

@gronxb is attempting to deploy a commit to the Callstack Team on Vercel.

A member of the Team first needs to authorize it.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 15 files

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 1 file (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

@thymikee

thymikee commented Nov 4, 2026

Copy link
Copy Markdown
Member

At c57133a, two issues need fixes before merge.

Could the HID contact owner guarantee a terminal event before dispatching touch-down? Touch-up construction can fail after down, including after a failed move, leaving UIKit with an active finger and subsequent interactions in the wrong state. Reserve and retain a usable release or cancel event across every exit, and test constructor failures after down to verify terminal dispatch before disposal.

The runner execution decision selects an eased HID schedule while successful edge flings report endpoint-hold. CLI JSON, Node and MCP consumers therefore receive incorrect timing metadata. Return the actual schedule through the backend result, preserve it in gestureResponseData, and amend ADR 0013 and the timing vocabulary to describe the edge schedule while keeping simulator policy out of shared semantic parsing.

Could the existing runner execution policy resolve contact kind and motion schedule together, return the schedule through backendResult, and keep event creation and guaranteed terminal dispatch in one HID contact owner? Align the execution-profile contract and ADR 0013 first, then remove competing timing inference when execution supplies its schedule.

Not blocking: You can take or leave an app-observed regression through the production gesture command that checks edge-recognizer began and ended, navigation pop, and ordinary swipe and pan controls, while retaining the planner and wire-contract checks.

The two existing threads no longer apply: constructor failure now propagates an error, and registry IDs now skip unreadable IDs.

Your app-observed report covers 3/3 edge cases on iOS 26.4, 27.2 and the Duo open and closed, with ordinary swipe and pan controls. Physical devices keep their previous route.

The only failing check is the Vercel deployment authorization, which is not a code problem. No conflicts are known. Before merge, please guarantee a terminal touch event, return the actual timing, and rerun the edge-recognizer cases with the swipe and pan controls on the fixed head.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 16 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread docs/adr/0013-unified-gesture-plans.md Resolved
@thymikee

thymikee commented Nov 4, 2026

Copy link
Copy Markdown
Member

At 48bd4f0, both earlier issues are fixed. A terminal event is reserved before touch-down and sent on every exit, and the runner now reports the schedule it actually ran. The new fault cases catch the old missing-terminal regression. Your live run on the native fix plus the metadata recheck at this head cover the change.

One design question before merge: could the contact function stay private to its owner? The new RunnerSimulatorEdgeTouch+Internal.h exports the contact function and HID table so the unit tests can inject faults, which ships a fault-injection interface in the runner. Making the function static and compiling the test facade only under AGENT_DEVICE_RUNNER_UNIT_TESTS would keep the guarantee and drop the export.

The earlier threads on contact failures, registry IDs, touching state and schedule names are fixed, and the ADR example now matches the code.

The only failing check is the Vercel deployment authorization, which is not a code problem. No conflicts are known.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

@thymikee

thymikee commented Nov 4, 2026

Copy link
Copy Markdown
Member

At 7a39ef0, the code looks good. The contact owner is now private to its file, and the fault-injection facade lives in UnitTests/, which the packaging script skips, so the runner no longer ships a test-only interface. This is a refactor with no behavior change, so your earlier live run on the native fix still covers the edge recognizers, navigation pop and the swipe and pan controls. The facade packaging thread no longer applies.

The only failing check is the Vercel deployment authorization, which is not a code problem. No conflicts are known.

@thymikee

thymikee commented Nov 4, 2026

Copy link
Copy Markdown
Member

At 9544fa2, the selector now accepts a touchscreen that names no Transport, but that path has only run against fake HID services. As the PR body says, no local host here reads without a Transport. Could the reporter on the iOS 18.1 or 26.5 host that failed with display 1 has no simulator touchscreen HID service run the edge-preset repro at this head? The run should show the selected service and its properties, both edge recognizers and the navigation pop, and the ordinary swipe and pan controls. Selector

Could the eligibility rule then be narrowed to what that host shows? Today a sole candidate is accepted whether its Transport is missing, another string or another type, and only the missing case has been reported. A positive identity for simulator touchscreens, kept in the one resolver, would avoid picking an unexpected sender silently.

The first-run left-edge failure after a boot or fold is worth keeping an eye on, but nothing ties it to this change. The only failing check is the Vercel deployment authorization, which is not a code problem. No conflicts are known.

@thymikee

thymikee commented Nov 4, 2026

Copy link
Copy Markdown
Member

At 8a57d06, a touchscreen without Transport is now used only when it is the display's sole candidate, which narrows the rule as asked. It is still selected only from fake HID services.

The newly accepted candidate has only been selected from fake HID services, so affected hosts may still fail to deliver screen-edge touches despite the passing unit tests (selection). Please run the edge-preset repro at this head on an affected iOS 18.1 or 26.5 host and capture the HID service inventory, the selected registry ID with Transport missing, and passing app-observed results for all three edge recognizer and navigation cases and the in-page controls.

Could touchscreen eligibility and ambiguity use one typed simulator service-identity selector, with diagnostics derived from its selection? First, establish a reliable identity for the missing-Transport service from affected-host evidence.

The only failing check is the Vercel deployment authorization, which is not a code problem. No conflicts are known.

gronxb added 11 commits November 5, 2026 01:40
XCTest synthesized touches carry no digitizer edge mask, so on an iOS
simulator gesture swipe left-edge and right-edge never started a
UIScreenEdgePanGestureRecognizer or the navigation edge pop gesture.

Edge swipe presets now plan a screen-edge contact. On an iOS simulator
the runner dispatches it as one HID touch with the edge mask, through
the touchscreen service of the app's display. Other gestures and
physical devices keep their current paths.

Fixes callstack#4208
A frame that failed to build was dropped while the gesture still
reported success. Fail before touch-down, lift early after a failed
move, and return the reason instead.
The edge recognizer on a rotated panel, such as the iPhone Duo inner
panel, never began: FromEdgeFlat/Tip touches carry no swipe direction,
and a fast first move left the edge region in one frame. A hand swipe
on the simulator carries SwipeLocked and the native swipe direction on
every frame and starts slowly.

Send the same mask, ease the contact in along the planned path, and use
the display's oldest touchscreen service when a window adds another.
A service whose registry ID could not be read still counted as a match,
so a later valid service could leave the sender at 0.
…vent

A touch-up that could not be built after touch-down left UIKit with an
active finger. The edge contact owner now builds a cancel event before
touch-down and, after touch-down, sends the touch-up or that cancel on
every exit. Unit tests fail each HID constructor in turn and check what
reaches the event system.
Edge flings ran an eased HID schedule but reported endpoint-hold. The
runner execution policy now resolves the contact and its schedule
together and returns the schedule as executionProfile. The daemon keeps
a reported schedule and infers the planned profile only when a backend
reports none. ADR 0013 records edge-ease-in and the reporting rule.
Address review: a backend-reported executionProfile is what ran, so the
daemon keeps any reported string and reports the planned profile only
when a backend reports none. Drop the TS schedule list that existed
only to filter it. The cancel test also checks that touch-down and each
move before the cancel touched.
Address review: the contact function and HID table were exported only so
unit tests could inject faults. Make them static and compile the test
facade in the same file under AGENT_DEVICE_RUNNER_UNIT_TESTS, so the
packaged runner ships no fault-injection interface.
…runner

Address review: the packager copies Objective-C sources verbatim, so the
AGENT_DEVICE_RUNNER_UNIT_TESTS block in RunnerSimulatorEdgeTouch.m
shipped and the package leak guard rejected it. Move the contact owner
into a header whose declarations all have internal linkage, include it
from the runner and from the test facade in UnitTests/, and keep the
shipped source free of the unit-test macro.
Reported on iOS 18.1 and 26.5 simulators: the display's touchscreen
service passed the usage, display and registry checks but read no
Transport, so edge swipes failed with "no simulator touchscreen HID
service". The oldest CoreDevice touchscreen still wins. Without one,
the display's only remaining candidate is used, and several are refused
as ambiguous with how each Transport read (missing, other type, other
value). The no-candidate error now names the stage that rejected the
touchscreens. Unit tests cover the reported service, CoreDevice
precedence, duplicates, ambiguity and the error.
…ole one

Address review: the fallback accepted a sole candidate whatever its
Transport read. Only a missing Transport has been reported, so a
touchscreen with a Transport of another value or type is no longer a
candidate, and the display refuses to pick a sender while one is
present without a CoreDevice touchscreen. The runner logs the selected
service and its properties as AGENT_DEVICE_RUNNER_EDGE_TOUCHSCREEN so a
host run shows which sender an edge touch used.
@gronxb
gronxb force-pushed the fix/ios-simulator-edge-swipe branch from 8a57d06 to 3b94f2e Compare November 4, 2026 17:24
@thymikee

thymikee commented Nov 4, 2026

Copy link
Copy Markdown
Member

At 3b94f2e, the code looks good and the rebase keeps the edge-contact behavior. The evidence I asked for in the earlier review is now in the PR body: the headless iOS 26.5 run at this head reaches the touchscreen that reports no Transport, and the same selector path ran on 26.4, 27.2 and the Duo at 8a57d06.

The CI jobs on this head are still queued, and none has failed. No conflicts are known.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Nov 4, 2026
@thymikee

thymikee commented Nov 5, 2026

Copy link
Copy Markdown
Member

At 7483e8d, the code looks good. The new commit is empty, so the tree is the same as 3b94f2e, which I reviewed earlier (#4211 (comment)). All 11 checks now pass and there are no conflicts.

@gronxb

gronxb commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

@thymikee here is evidence real computer

As-is (agent-device 0.21.36) To-be (#4211)
2026-11-04.1.30.10.mov
to-be.mov
edge: 0, edge recognizer never began, no back pop edge: 1, edge recognizer began, back pop

@thymikee
thymikee merged commit ae1a19c into callstack:main Nov 5, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS simulator edge swipes never start UIScreenEdgePanGestureRecognizer

2 participants