Add expanded/collapsed state support for SwiftUI DisclosureGroup#324
Draft
RoyalPineapple wants to merge 11 commits intomainfrom
Draft
Add expanded/collapsed state support for SwiftUI DisclosureGroup#324RoyalPineapple wants to merge 11 commits intomainfrom
RoyalPineapple wants to merge 11 commits intomainfrom
Conversation
21ce7c6 to
2e59b92
Compare
SwiftUI DisclosureGroup and expandable list sections communicate their expanded/collapsed state to VoiceOver through _accessibilityExpandedStatus, but our parser didn't read this property. Snapshots for screens with expandable content were missing this critical state information. - Add ExpandedStatus enum (unsupported/expanded/collapsed) to AccessibilityMarker - Read _accessibilityExpandedStatus via KVC on each accessibility element - Include "Expanded." / "Collapsed." in the VoiceOver description text - Add localized strings for en, de, ru - Add SwiftUIDisclosureGroup demo view and snapshot test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
VoiceOver announces "Double tap to collapse/expand" as a hint on DisclosureGroup elements. Include this hint in the snapshot output alongside the existing Expanded/Collapsed trait specifier, appending to any existing hint rather than overwriting it. Regenerate reference images for all three OS versions (17.5, 18.5, 26.2). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d status findings Adds interactive test UI for investigating _accessibilityExpandedStatus behavior on real devices, and updates doc comments across the parser to reflect findings: - _accessibilityExpandedStatus is the VoiceOver source of truth (since iOS 14.2) - Public iOS 18 accessibilityExpandedStatus syncs TO private, not vice versa - SwiftUI DisclosureGroup only sets the private API Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
e9cb098 to
6ad2488
Compare
…Object KVC crash - Restore SwiftUIDisclosureGroup.swift to its minimal form so it matches the committed reference images. Interactive mutation test UI was used for on-device investigation; findings are captured in commit messages and docs. - testNSObjectExpandedStatusBehavior was crashing because plain NSObject is not KVC-compliant for _accessibilityExpandedStatus. Switch to method(for:) + unsafeBitCast to call the method directly without KVC. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a PrivateAXSelector protocol + catalog that wraps every private API call in a type-safe, crash-free invocation (uses method(for:) + unsafeBitCast for primitives, perform() for objects — never KVC). NSObject.ax_private<S> is the single entry point; each selector carries its return type. Migrates the expanded-status read to this wrapper and removes the duplicated inline KVC access in UIAccessibility+SnapshotAdditions.swift. The parser now reads _accessibilityExpandedStatus once per element and threads the typed ExpandedStatus through accessibilityDescription(context:expandedStatus:), eliminating two additional reads and the latent NSUnknownKeyException risk. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Captures three findings from the on-device investigation: 1. The private method is what VoiceOver actually reads. 2. The public iOS 18 getter has separate storage and misses SwiftUI overrides. 3. When the two disagree, VoiceOver announces based on the private value. Also points the parser's read site to PrivateAX.ExpandedStatus for context. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Deletes the 832-line ExpandedStatusDiagnosticTests.swift (pure print-based investigation with few assertions), trims ExpandedStatusSyncTests.swift to the four tests that guard real invariants, and adds ExpandedStatusDescriptionTests.swift with direct string-level coverage of the description/hint output — including the hint concatenation path. Also drops the unused PrivateAXObjectSelector protocol and softens an unsourced iOS 14.2 claim in the PrivateAX.ExpandedStatus doc comment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The tests reference the public iOS 18 `accessibilityExpandedStatus` property, which only exists in the iOS 18 SDK. The iOS 17 CI job runs on macOS-14 with Xcode 15, which can't resolve the symbol at compile time. The tests are already `@available(iOS 18.0, *)`, so gating the file with `#if compiler(>=6.0)` (same pattern used in AccessibilityHierarchy+Codable.swift) is compile-only and doesn't reduce coverage on runners that can actually execute them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Committed reference images for SwiftUIDisclosureGroupTests don't match iOS 17.5 / 18.5 CI renders. Enable recordMode so CI writes fresh captures to FB_REFERENCE_IMAGE_DIR; the failure-path "Reference Images" artifact upload will then contain the correct references. Next commit will harvest the images and flip recordMode off. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
Adds support for reading the expanded/collapsed state from SwiftUI
DisclosureGroupand expandable list sections, so accessibility snapshots now show whether these elements are expanded or collapsed — matching what VoiceOver announces to users.What changed
ExpandedStatusenum onAccessibilityElement—unsupported,expanded,collapsed_accessibilityExpandedStatusvia the ObjC runtime on each accessibility element during parsing"Section. Button. Heading. Expanded.""Double tap to collapse."/"Double tap to expand.", appended to any existing hintDisclosureGroupwith reference images on iOS 17.5, 18.5, 26.2Why the private API
The public iOS 18
accessibilityExpandedStatusproperty exists but does not work on SwiftUI nodes — SwiftUI'sAccessibilityNodeoverrides only the private_accessibilityExpandedStatusmethod. We verified this thoroughly through runtime introspection:NSObject_accessibilityExpandedStatus(getter) andaccessibilityExpandedStatus(getter/setter) are both declared directly onNSObjectUIView/NSObject, setting the public property always updates the private getter — they share backing storage_setAccessibilityExpandedStatus:does not exist on any class; the private key is also not KVC-compliantSwiftUI.AccessibilityNodeoverrides_accessibilityExpandedStatusto read from SwiftUI's internal accessibility graph, bypassing the shared storage entirely_accessibilityExpandedStatusreturns — confirmed on-device with iPhone 15Conclusion: Reading
_accessibilityExpandedStatusis correct because it is the single source of truth that both SwiftUI (via subclass override) and UIKit (via the public setter's internal sync) funnel into. The public API is actually less reliable — it misses all SwiftUI elements entirely.We use
responds(to:)+method(for:)to read the value, consistent with how the codebase already reads other private accessibility properties.Data flow
flowchart LR A["SwiftUI DisclosureGroup"] -->|"_accessibilityExpandedStatus"| B["NSObject extension\n(runtime read)"] B -->|"ExpandedStatus enum"| C["AccessibilityElement\n.expandedStatus"] B -->|"rawStatus 1/2"| D["Description: 'Expanded.' / 'Collapsed.'"] B -->|"rawStatus 1/2"| E["Hint: 'Double tap to collapse/expand.'"] D --> F["Snapshot Legend"] E --> FSnapshot output
Before:
After:
Test plan
SwiftUIDisclosureGroupTestssnapshot test passes on iOS 17.5, 18.5, 26.2