Skip to content

Filter non-ItemView subviews out of accessibilityElements - #345

Merged
brynbodayle merged 1 commit into
airbnb:masterfrom
jqsilver:ab-filter-non-itemview-subviews
Aug 12, 2026
Merged

brynbodayle merged 1 commit into
airbnb:masterfrom
jqsilver:ab-filter-non-itemview-subviews

Conversation

@jqsilver

@jqsilver jqsilver commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Details

CalendarScrollView.accessibilityElements force-casts every subview to [ItemView], and fatalErrors if the cast fails:

guard let itemViews = subviews as? [ItemView] else {
  fatalError("Only `ItemView`s can be used as subviews of the scroll view.")
}

That invariant holds only while HorizonCalendar is the sole contributor of subviews. On iOS 27, UIKit adds two _UITouchPassthroughViews to the scroll view, so the cast fails and the app dies:

HorizonCalendar/CalendarScrollView.swift:55: Fatal error:
Only `ItemView`s can be used as subviews of the scroll view.

This filters to the item views instead of requiring that every subview is one:

let itemViews = subviews.compactMap { $0 as? ItemView }

I left the fatalError in the sorted closure below alone, since the filter above it means it is unreachable.

I also considered pairing the filter with an assertionFailure, to keep a development signal that an unexpected subview showed up. It seemed wrong here: on iOS 27 the unexpected subviews are normal and arrive on every calendar, so the assert would fire constantly while telling developers about something they cannot act on.

Related Issue

None filed. Happy to open one if you would prefer to track it separately.

Motivation and Context

This is a crash rather than a cosmetic problem, and because it is a fatalError inside the library, a consumer cannot work around it — there is no way to stop UIKit from adding those subviews, and no way to avoid the property being read.

Only the accessibility path reads accessibilityElements, so it stays hidden until something queries accessibility. It surfaced for us in accessibility snapshot tests on Xcode 27, which took out every calendar-bearing screen we snapshot while everything else stayed green. It should equally affect real users who have Voice Over enabled on iOS 27.

The two views UIKit adds are private hit-testing views that carry no content, so filtering them out produces the same element set that the force-cast produced on iOS 26 and earlier. Voice Over behavior is unchanged.

We are running this as a local patch against 470234795d31 at Airbnb to unblock our Xcode 27 work.

How Has This Been Tested

  • Added CalendarScrollViewTests, which adds a plain UIView alongside an ItemView and asserts that the item view is still returned. It does not depend on UIKit inserting anything, so it reproduces the crash on any OS version. I checked that it does — reverting just the fix and rerunning it on iOS 26 crashes in the test:

    HorizonCalendar/CalendarScrollView.swift:55: Fatal error:
    Only `ItemView`s can be used as subviews of the scroll view.
    
  • Ran the full test suite on iOS 26 (xcodebuild test -destination "name=iPhone 16"), 107 tests green.

  • The test file needed adding to the test target in HorizonCalendar.xcodeproj, which is the only reason the project file is in this diff.

  • Verified the fix end to end in the Airbnb app under Xcode 27.0 beta 4 on an iOS 27.0 simulator. Before: Fatal error, and the accessibility snapshot suite executed 0 tests. After: the suite passes and captures its snapshots.

  • Confirmed on iOS 26 that snapshots are byte-identical with and without the change, so this is not a behavior change on shipping OS versions.

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.

One note on the CONTRIBUTING checklist: it asks for a version bump in the Xcode project and podspec, and I have not done one. The recent merged PRs (#338, #340, #342) update only the CHANGELOG under [Unreleased], so I followed that. Glad to add a bump if you want it.

Please Review

@bryankeller @brynbodayle @marybm-dev

`CalendarScrollView.accessibilityElements` force-casts every subview to
`[ItemView]` and `fatalError`s if the cast fails. That invariant holds only while
HorizonCalendar is the sole contributor of subviews. On iOS 27 UIKit adds two
`_UITouchPassthroughView`s to the scroll view, so the cast fails and the app
dies:

    HorizonCalendar/CalendarScrollView.swift:55: Fatal error:
    Only `ItemView`s can be used as subviews of the scroll view.

Filters to the item views instead of requiring that every subview is one. The
views UIKit adds are private hit-testing views with no content, so the resulting
element set matches what the force-cast produced on earlier OS versions.

Only the accessibility path reads this property, so it stays hidden until
something queries accessibility.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jqsilver
jqsilver marked this pull request as ready for review August 12, 2026 20:30

@brynbodayle brynbodayle 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.

thanks!

@brynbodayle
brynbodayle merged commit 363088c into airbnb:master Aug 12, 2026
2 checks passed
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.

2 participants