From 9d61517f90b94af4eb030fc981621040e8cd447a Mon Sep 17 00:00:00 2001 From: Frank Osterfeld Date: Wed, 9 Jul 2025 13:14:57 +0200 Subject: [PATCH] iOS: Accessibility fixes When embedding native views as subviews, make sure that their accessibility elements are reported and thus used by iOS when navigating e.g. via VoiceOver. Implement accessibilityHitTest to avoid iOS picking accessibility items below other items that should have priority. Task-Id: QTBUG-138496 Task-Id: QTBUG-138406 Change-Id: Iebd7a6a76cbcddc616e61568d464e10480c96d8e --- src/plugins/platforms/ios/quiview.mm | 7 ++- .../platforms/ios/quiview_accessibility.mm | 62 +++++++++++++++++-- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/plugins/platforms/ios/quiview.mm b/src/plugins/platforms/ios/quiview.mm index b31e1a043dc..41774c2bc1e 100644 --- a/src/plugins/platforms/ios/quiview.mm +++ b/src/plugins/platforms/ios/quiview.mm @@ -54,7 +54,9 @@ inline ulong getTimeStamp(UIEvent *event) @implementation QUIView { QHash m_activeTouches; UITouch *m_activePencilTouch; - NSMutableArray *m_accessibleElements; + NSMutableArray *m_accessibleElements; + bool m_accessibleElementsQtCreated; + bool m_accessibleElementsSubviewsCreated; UIPanGestureRecognizer *m_scrollGestureRecognizer; CGPoint m_lastScrollCursorPos; CGPoint m_lastScrollDelta; @@ -101,7 +103,8 @@ - (instancetype)initWithQIOSWindow:(QT_PREPEND_NAMESPACE(QIOSWindow) *)window #ifndef Q_OS_TVOS self.multipleTouchEnabled = YES; #endif - + m_accessibleElementsQtCreated = false; + m_accessibleElementsSubviewsCreated = false; m_scrollGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleScroll:)]; diff --git a/src/plugins/platforms/ios/quiview_accessibility.mm b/src/plugins/platforms/ios/quiview_accessibility.mm index a95610614be..7728b0ea4e6 100644 --- a/src/plugins/platforms/ios/quiview_accessibility.mm +++ b/src/plugins/platforms/ios/quiview_accessibility.mm @@ -15,6 +15,7 @@ - (void)createAccessibleElement:(QAccessibleInterface *)iface QAccessible::Id accessibleId = QAccessible::uniqueId(iface); UIAccessibilityElement *elem = [QT_MANGLE_NAMESPACE(QMacAccessibilityElement) elementWithId:accessibleId]; [m_accessibleElements addObject:elem]; + m_accessibleElementsQtCreated = true; } - (void)createAccessibleContainer:(QAccessibleInterface *)iface @@ -41,18 +42,71 @@ - (void)initAccessibility QGuiApplicationPrivate::platformIntegration()->accessibility()->setActive(true); init = true; - if ([m_accessibleElements count]) - return; + if (!m_accessibleElementsQtCreated) { + QWindow *win = self.platformWindow->window(); + QAccessibleInterface *iface = win->accessibleRoot(); + if (iface) + [self createAccessibleContainer: iface]; + } + + if (!m_accessibleElementsSubviewsCreated) { + for (UIView * subview in self.subviews) { + if (!subview.hidden && subview.alpha >= 0.01) { + [m_accessibleElements addObject:subview]; + } + } + m_accessibleElementsSubviewsCreated = true; + } +} + +- (void)didAddSubview:(UIView *)subview { + [super didAddSubview:subview]; + [self clearAccessibleCache]; +} +- (void)willRemoveSubview:(UIView *)subview { + [super willRemoveSubview:subview]; + [self clearAccessibleCache]; +} + +- (id)accessibilityHitTest:(CGPoint)point withEvent:(UIEvent *)event { + for (UIView* subview in self.subviews) { + CGPoint pointInSubview = [subview convertPoint:point fromView:self]; + if (CGRectContainsPoint(subview.bounds, pointInSubview)) { + return [subview accessibilityHitTest:point withEvent:event]; + } + } + if (!self.platformWindow) { + return nil; + } QWindow *win = self.platformWindow->window(); QAccessibleInterface *iface = win->accessibleRoot(); - if (iface) - [self createAccessibleContainer: iface]; + if (!iface) { + return nil; + } + + QPoint screenPoint = self.platformWindow->mapToGlobal(QPoint(point.x, point.y)); + QAccessibleInterface *child = iface->childAt(screenPoint.x(), screenPoint.y()); + + if (!child) { + return nil; + } + + QAccessible::Id childId = QAccessible::uniqueId(child); + for (id object in m_accessibleElements) { + if ([object isKindOfClass:[QMacAccessibilityElement class]] && ((QMacAccessibilityElement*)object).axid == childId) { + return object; + } + } + + return nil; } - (void)clearAccessibleCache { [m_accessibleElements removeAllObjects]; + m_accessibleElementsQtCreated = false; + m_accessibleElementsSubviewsCreated = false; } // this is a container, returning yes here means the functions below will never be called -- 2.39.5 (Apple Git-154)