Skip to content

fix: guard accessibility updates behind window != nil#613

Draft
RoyalPineapple wants to merge 1 commit intomainfrom
alex/receiver-traits
Draft

fix: guard accessibility updates behind window != nil#613
RoyalPineapple wants to merge 1 commit intomainfrom
alex/receiver-traits

Conversation

@RoyalPineapple
Copy link
Copy Markdown
Collaborator

@RoyalPineapple RoyalPineapple commented Apr 22, 2026

Problem

AXRuntime maintains an internal element cache (a map table) that is allocated during the scene lifecycle. Under XCTest's test runner, the scene lifecycle is delayed — the cache is still NULL when the first accessibility broadcast fires, and NSMapGet crashes on the NULL pointer.

NSMapGet (x0 = 0x0, NULL map table)
  ← AXRuntime: _AXIsInElementCache
    ← AXRuntime: __AXCreateAXUIElementWithElement
      ← UIAccessibility: _axuiElementForNotificationData
        ← UIAccessibility: _massageNotificationDataBeforePost
          ← UIAccessibility: _UIAXBroadcastMainThread
            ← libdispatch: _dispatch_main_queue_drain

Root cause

ReceiverContainerView.init sets isAccessibilityElement = true during init. When this view is added to the hierarchy via addSubview, UIKit internally posts an accessibility notification. Under XCTest's test runner, AXRuntime's element cache hasn't been allocated yet when that notification fires during a run-loop drain, crashing on the NULL map table.

We can't guard UIKit's internal notification post — the fix is to not set isAccessibilityElement = true until the view is in a window, at which point the scene lifecycle has completed and AXRuntime is initialized.

Evidence

  • Crash reproduced 100% when the app is launched via xcodebuild test
  • Does NOT reproduce via xcrun simctl launch (normal launch)
  • Confirmed with LLDB: conditional breakpoint on NSMapGet where x0 == 0 hit the NULL map table
  • Only appeared after adopting .deferredAccessibilityReceiver() on views present during initial layout

Fix

  • ReceiverContainerView: move isAccessibilityElement = true from init to didMoveToWindow(). The view only becomes an accessibility element after it has a window. Sets isAccessibilityElement = false when removed from a window. Also defers needsAccessibilityUpdate = true to didMoveToWindow() so the first accessibility update doesn't trigger before the scene is ready. Removed the redundant isAccessibilityElement = true and needsAccessibilityUpdate = true from the backingViewDescription apply block since didMoveToWindow handles both.

  • CombinableView.updateAccessibility(), ReceiverContainerView.updateDeferredAccessibility(), SourceContainerView.updateAccessibility(): early-return when window == nil. Defensive guards to prevent unnecessary accessibility work on views not in the hierarchy.

  • Add missing accessibilityTraits override on CombinableView: the getter was missing the lazy-update pattern that all other accessibility property overrides use. Added for consistency.

@RoyalPineapple RoyalPineapple changed the title fix: CombinableView accessibility traits and ReceiverContainer traits param fix: lazy-update accessibilityTraits on CombinableView Apr 22, 2026
@RoyalPineapple RoyalPineapple marked this pull request as ready for review April 22, 2026 15:37
@RoyalPineapple RoyalPineapple requested a review from a team as a code owner April 22, 2026 15:37
@RoyalPineapple RoyalPineapple changed the title fix: lazy-update accessibilityTraits on CombinableView fix: accessibility traits lazy-update and window guard for AXRuntime crash Apr 23, 2026
@RoyalPineapple RoyalPineapple changed the title fix: accessibility traits lazy-update and window guard for AXRuntime crash fix: guard accessibility updates behind window != nil Apr 23, 2026
@RoyalPineapple RoyalPineapple force-pushed the alex/receiver-traits branch 5 times, most recently from ef494f7 to 3d5d788 Compare April 23, 2026 13:30
@RoyalPineapple RoyalPineapple marked this pull request as draft April 23, 2026 14:26
…indow

Prevents an AXRuntime crash (NSMapGet on a NULL element cache) when
UIKit posts an internal accessibility notification in response to
isAccessibilityElement = true on a view being added to the hierarchy.

Under XCTest's test runner, the scene lifecycle is delayed and
AXRuntime's element cache is still NULL when the notification fires.

Move isAccessibilityElement = true from init to didMoveToWindow so
the view only becomes an accessibility element after it has a window
and AXRuntime is initialized.

Guard CombinableView.isAccessibilityElement getter to return false
when window is nil, ensuring UIKit never considers the view an
accessibility element before the scene lifecycle is ready.

Also guards CombinableView.updateAccessibility(),
ReceiverContainerView.updateDeferredAccessibility(), and
SourceContainerView.updateAccessibility() behind window != nil to
prevent unnecessary accessibility work on views not in the hierarchy.

Adds the missing accessibilityTraits lazy-update override on
CombinableView for consistency with other property getters.
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