Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed a performance issue caused by the item models for day ranges and month backgrounds not being cached
- Fixed an issue that could cause the calendar to appear blank after rotating the device
- Fixed an issue with touch handling on iOS 26
- Fixed a crash when accessibility is enabled on iOS 27, caused by assuming that every subview of the calendar's scroll view is an item view

### Changed
- Rewrote accessibility code to avoid posting notifications, which causes poor Voice Over performance and odd focus bugs
Expand Down
4 changes: 4 additions & 0 deletions HorizonCalendar.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
086333B02AB8D36900CC6125 /* CalendarContentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086333AE2AB8D34700CC6125 /* CalendarContentTests.swift */; };
AB1200000000000000000002 /* CalendarScrollViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1200000000000000000001 /* CalendarScrollViewTests.swift */; };
9321957E26EEB44C0001C7E9 /* DayOfWeekView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9321957D26EEB44C0001C7E9 /* DayOfWeekView.swift */; };
9321958026EEB6330001C7E9 /* Shape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9321957F26EEB6330001C7E9 /* Shape.swift */; };
9321958226EEB6AB0001C7E9 /* DrawingConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9321958126EEB6AB0001C7E9 /* DrawingConfig.swift */; };
Expand Down Expand Up @@ -88,6 +89,7 @@

/* Begin PBXFileReference section */
086333AE2AB8D34700CC6125 /* CalendarContentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarContentTests.swift; sourceTree = "<group>"; };
AB1200000000000000000001 /* CalendarScrollViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarScrollViewTests.swift; sourceTree = "<group>"; };
9321957D26EEB44C0001C7E9 /* DayOfWeekView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayOfWeekView.swift; sourceTree = "<group>"; };
9321957F26EEB6330001C7E9 /* Shape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shape.swift; sourceTree = "<group>"; };
9321958126EEB6AB0001C7E9 /* DrawingConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawingConfig.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -243,6 +245,7 @@
939E694D2484B14400A8BCC7 /* VisibleItemsProviderTests.swift */,
9396F3D22483261B008AD306 /* Info.plist */,
086333AE2AB8D34700CC6125 /* CalendarContentTests.swift */,
AB1200000000000000000001 /* CalendarScrollViewTests.swift */,
);
path = Tests;
sourceTree = "<group>";
Expand Down Expand Up @@ -487,6 +490,7 @@
939E695C2484B22600A8BCC7 /* VisibleItemsProviderTests.swift in Sources */,
93A0062C24F206BE00F667A3 /* ItemViewReuseManagerTests.swift in Sources */,
086333B02AB8D36900CC6125 /* CalendarContentTests.swift in Sources */,
AB1200000000000000000002 /* CalendarScrollViewTests.swift in Sources */,
939E69592484B21700A8BCC7 /* LayoutItemTypeEnumeratorTests.swift in Sources */,
93FA64F2248D93EA00A8B7B1 /* MonthRowTests.swift in Sources */,
932E24142558DF6E001648D2 /* HorizontalMonthsLayoutOptionsTests.swift in Sources */,
Expand Down
6 changes: 3 additions & 3 deletions Sources/Internal/CalendarScrollView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ final class CalendarScrollView: UIScrollView {

override var accessibilityElements: [Any]? {
get {
guard let itemViews = subviews as? [ItemView] else {
fatalError("Only `ItemView`s can be used as subviews of the scroll view.")
}
// UIKit can add subviews of its own - on iOS 27 it adds `_UITouchPassthroughView`s - so we
// filter to the item views rather than requiring that every subview is one.
let itemViews = subviews.compactMap { $0 as? ItemView }
cachedAccessibilityElements = cachedAccessibilityElements ?? itemViews
.filter {
switch $0.itemType {
Expand Down
78 changes: 78 additions & 0 deletions Tests/CalendarScrollViewTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Created by Andy Bartholomew on 8/12/26.
// Copyright © 2026 Airbnb Inc. All rights reserved.

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import XCTest
@testable import HorizonCalendar

// MARK: - CalendarScrollViewTests

final class CalendarScrollViewTests: XCTestCase {

// MARK: Internal

func testAccessibilityElementsIgnoresSubviewsThatAreNotItemViews() {
let scrollView = CalendarScrollView()
let itemView = itemView(for: .monthHeader(month))
scrollView.addSubview(itemView)

// UIKit adds subviews of its own on some OS versions - `_UITouchPassthroughView`s on iOS 27, for
// example - which must not prevent the item views from being returned.
scrollView.addSubview(UIView())

XCTAssertEqual(scrollView.accessibilityElements?.count, 1)
XCTAssert(scrollView.accessibilityElements?.first as? ItemView === itemView)
}

func testAccessibilityElementsWithNoItemViews() {
let scrollView = CalendarScrollView()
scrollView.addSubview(UIView())

XCTAssertEqual(scrollView.accessibilityElements?.count, 0)
}

// MARK: Private

private let month = Month(era: 1, year: 2026, month: 08, isInGregorianCalendar: true)

private func itemView(for itemType: LayoutItem.ItemType) -> ItemView {
let itemView = ItemView(initialCalendarItemModel: MockCalendarItemModel())
itemView.itemType = .layoutItemType(itemType)
return itemView
}

}

// MARK: - MockCalendarItemModel

private struct MockCalendarItemModel: AnyCalendarItemModel {

var _itemViewDifferentiator = _CalendarItemViewDifferentiator(
viewType: ObjectIdentifier(UIView.self),
invariantViewProperties: AnyHashable(0)
)

func _makeView() -> UIView {
UIView()
}

func _setContent(onViewOfSameType _: UIView) { }

func _isContentEqual(toContentOf _: AnyCalendarItemModel) -> Bool {
false
}

mutating func _setSwiftUIWrapperViewContentIDIfNeeded(_: AnyHashable) { }

}
Loading