Skip to content

Prune views when either frame axis is degenerate#341

Draft
RoyalPineapple wants to merge 6 commits into
mainfrom
RoyalPineapple/zero-width-elements
Draft

Prune views when either frame axis is degenerate#341
RoyalPineapple wants to merge 6 commits into
mainfrom
RoyalPineapple/zero-width-elements

Conversation

@RoyalPineapple
Copy link
Copy Markdown
Collaborator

@RoyalPineapple RoyalPineapple commented May 27, 2026

Summary

The view-pruning gate in recursiveAccessibilityHierarchy checked frame.size == .zero, which only matched when both width and height were zero. Slivers with one axis at zero (e.g. width 0, height 100) slipped through and surfaced as elements with no visible footprint.

Switch the check to compare each axis independently — prune when either frame.width or frame.height is below a sub-pixel threshold (0.001). This catches single-axis collapses and float-layout slivers in addition to exact zero.

Test plan

  • Existing snapshot/unit tests pass
  • Verify a view with a zero-width-but-nonzero-height frame is no longer surfaced as an accessibility element

The pruning gate in recursiveAccessibilityHierarchy previously only fired
when the view's frame size was fully `.zero`, so slivers with one axis at
zero (e.g. width 0, height 100) slipped through and produced spurious
elements with no visible footprint. Check each axis independently against
a sub-pixel threshold (0.001) to catch float-layout slivers in addition to
exact zero.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@RoyalPineapple RoyalPineapple changed the title Prune views with degenerate accessibility frames on either axis Prune views when either frame axis is degenerate May 27, 2026
RoyalPineapple and others added 5 commits May 27, 2026 14:05
elementB previously had width 0 — incidental to what the test exercises
(vertical separation sort order), but now pruned by the degenerate-frame
gate. Give it a real 10pt width; positions still don't overlap A/C/D and
the sort-order assertion is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous revision broadened the pruning gate to drop any view with a
single-axis-degenerate frame when it clipped, was an accessibility element,
or exposed accessibility children. SwiftUI's bridging layers produce
intermediate wrapper views with single-axis-degenerate frames that clip
but contain real, visible children — those were being pruned, eliminating
the entire ScrollView content of PathShapesDemo (verified via the failing
testPathShapesDemo snapshot diff on iOS 18 / 26: only the DemoSection's
combined element survived, the 12 shape views disappeared).

Keep the original strict frame.size == .zero pruning for clipping wrappers
and accessibility containers, and apply the per-axis sub-pixel threshold
only to isAccessibilityElement views — which is what the user actually
wanted filtered out as spurious targets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract isInvisible, isCollapsedWrapper, and isDegenerateElement into named
locals so each prune rule is self-documenting. Use abs(...) <= 0.001 to mirror
VoiceOver's degenerate-frame check (handles negative-width edge cases that can
arise from pathological transforms or direct frame assignment).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the abs/threshold width/height checks with view.frame.isEmpty
(true when either dimension is <= 0). Same behavior for the degenerate-
axis case, plus consistent handling of negative-dimension pathology, and
the leaf rule now matches the wrapper rule's frame check.

Co-Authored-By: Claude Opus 4.7 (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