Skip to content

Surface SwiftUI swipe actions via _privateAccessibilityCustomActions#323

Draft
RoyalPineapple wants to merge 3 commits intomainfrom
RoyalPineapple/swipe-a11y-actions
Draft

Surface SwiftUI swipe actions via _privateAccessibilityCustomActions#323
RoyalPineapple wants to merge 3 commits intomainfrom
RoyalPineapple/swipe-a11y-actions

Conversation

@RoyalPineapple
Copy link
Copy Markdown
Collaborator

@RoyalPineapple RoyalPineapple commented Mar 25, 2026

Problem

SwiftUI .swipeActions are invisible to the accessibility snapshot parser. VoiceOver surfaces them, but the parser only reads accessibilityCustomActions — which doesn't include swipe actions.

Root cause

UIKit exposes SwiftUI swipe actions through a private API: _privateAccessibilityCustomActions on NSObject. These actions live on the container view (e.g. ListCollectionViewCell), not on the accessibility element proxies inside it:

UICollectionView (List)
 └─ ListCollectionViewCell          ← _privateAccessibilityCustomActions: ["Delete"]
     └─ AccessibilityNode (proxy)   ← accessibilityCustomActions: []
         label: "Row Label"

The parser walks the proxy elements but never looks at the container — so swipe actions are silently dropped.

Solution

Capture private custom actions at the container level in the accessibility model:

AccessibilityContainer
 ├─ customActions: ["Delete"]     ← NEW: captured from _privateAccessibilityCustomActions
 └─ children:
     └─ AccessibilityElement
         └─ customActions: []     ← public actions only (unchanged)

VoiceOver behavior (verified on device)

  • Private actions are presented first, before public actions
  • No deduplication — if a swipe action and a public action share the same name, both appear
  • Private actions on a container are inherited by all child elements

allCustomActions on NSObject matches this ordering and dedup behavior.

What's deferred

Container-aware rendering is future work. The snapshot reference images are identical before and after the parser fix — the model now correctly captures swipe actions, but they won't appear visually in snapshots until the renderer is updated to display container-level actions. Depends on #278 (container visualization) landing first.

Commit structure

Commit Purpose
99083951 Add SwiftUI swipe actions demo and snapshot tests Demo + snapshots showing the gap (actions missing)
718686a7 Read swipe actions via _privateAccessibilityCustomActions Parser fix + unit tests verifying container capture

Dependencies

Test plan

  • SwipeActionsParserTests — container captures private actions, element public actions preserved, multi-element row inheritance, no-swipe baseline
  • Snapshot tests pass on iOS 17.5, 18.5, 26.2
  • CI green

🤖 Generated with Claude Code

@RoyalPineapple RoyalPineapple changed the title Add SwiftUI swipe actions demo and a11y tree dump test [Research ]Add SwiftUI swipe actions demo and a11y tree dump test Mar 26, 2026
@RoyalPineapple RoyalPineapple changed the title [Research ]Add SwiftUI swipe actions demo and a11y tree dump test Fix parser to detect SwiftUI swipe actions as custom actions Mar 26, 2026
@RoyalPineapple RoyalPineapple changed the title Fix parser to detect SwiftUI swipe actions as custom actions Surface SwiftUI swipe actions via private accessibility API Mar 26, 2026
@RoyalPineapple RoyalPineapple changed the title Surface SwiftUI swipe actions via private accessibility API Surface SwiftUI swipe actions via _privateAccessibilityCustomActions Mar 26, 2026
@RoyalPineapple RoyalPineapple force-pushed the RoyalPineapple/swipe-a11y-actions branch from 63c1660 to da313dc Compare March 26, 2026 18:14
@RoyalPineapple RoyalPineapple marked this pull request as ready for review March 26, 2026 18:14
@RoyalPineapple RoyalPineapple marked this pull request as draft March 26, 2026 18:52
RoyalPineapple and others added 2 commits March 27, 2026 15:14
Adds a demo screen exercising SwiftUI .swipeActions with three cases:
swipe-only, swipe + public action (same name), and swipe + public
action (different names). Snapshot tests and reference images capture
the current state where swipe actions are NOT surfaced by the parser.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SwiftUI .swipeActions are exposed through NSObject's private
_privateAccessibilityCustomActions, which lives on container views
(e.g. ListCollectionViewCell) rather than on individual accessibility
elements. Captures these at the container level in
AccessibilityContainer.customActions.

VoiceOver presents private actions first and does not deduplicate
against public actions with the same name — allCustomActions on
NSObject now matches this behavior.

Container-aware rendering (displaying these in snapshots) is deferred
to future work.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@RoyalPineapple RoyalPineapple force-pushed the RoyalPineapple/swipe-a11y-actions branch from da313dc to 718686a Compare March 27, 2026 14:16
…tions

The new `!customActions.isEmpty` guard in `containerInfo(for:)` can return
a ContainerInfo for .none-typed views that carry private custom actions
(e.g. SwiftUI swipe actions). Update the comment and doc to reflect this.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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