| Flutter iOS Embedder |

SemanticsObject.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h"
6
7 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"
8 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h"
9
10 FLUTTER_ASSERT_ARC
11
12 namespace {
13
14 flutter::SemanticsAction GetSemanticsActionForScrollDirection(
15 UIAccessibilityScrollDirection direction) {
16 // To describe the vertical scroll direction, UIAccessibilityScrollDirection uses the
17 // direction the scroll bar moves in and SemanticsAction uses the direction the finger
18 // moves in. However, the horizontal scroll direction matches the SemanticsAction direction.
19 // That is way the following maps vertical opposite of the SemanticsAction, but the horizontal
20 // maps directly.
21 switch (direction) {
22 case UIAccessibilityScrollDirectionRight:
23 case UIAccessibilityScrollDirectionPrevious: // TODO(abarth): Support RTL using
24 // _node.textDirection.
25 return flutter::SemanticsAction::kScrollRight;
26 case UIAccessibilityScrollDirectionLeft:
27 case UIAccessibilityScrollDirectionNext: // TODO(abarth): Support RTL using
28 // _node.textDirection.
29 return flutter::SemanticsAction::kScrollLeft;
30 case UIAccessibilityScrollDirectionUp:
31 return flutter::SemanticsAction::kScrollDown;
32 case UIAccessibilityScrollDirectionDown:
33 return flutter::SemanticsAction::kScrollUp;
34 }
35 FML_DCHECK(false); // Unreachable
36 return flutter::SemanticsAction::kScrollUp;
37 }
38
39 SkM44 GetGlobalTransform(SemanticsObject* reference) {
40 SkM44 globalTransform = [reference node].transform;
41 for (SemanticsObject* parent = [reference parent]; parent; parent = parent.parent) {
42 globalTransform = parent.node.transform * globalTransform;
43 }
44 return globalTransform;
45 }
46
47 SkPoint ApplyTransform(SkPoint& point, const SkM44& transform) {
48 SkV4 vector = transform.map(point.x(), point.y(), 0, 1);
49 return SkPoint::Make(vector.x / vector.w, vector.y / vector.w);
50 }
51
52 CGPoint ConvertPointToGlobal(SemanticsObject* reference, CGPoint local_point) {
53 SkM44 globalTransform = GetGlobalTransform(reference);
54 SkPoint point = SkPoint::Make(local_point.x, local_point.y);
55 point = ApplyTransform(point, globalTransform);
56 // `rect` is in the physical pixel coordinate system. iOS expects the accessibility frame in
57 // the logical pixel coordinate system. Therefore, we divide by the `scale` (pixel ratio) to
58 // convert.
59 UIScreen* screen = reference.bridge->view().window.screen;
60 // Screen can be nil if the FlutterView is covered by another native view.
61 CGFloat scale = (screen ?: UIScreen.mainScreen).scale;
62 auto result = CGPointMake(point.x() / scale, point.y() / scale);
63 return [reference.bridge->view() convertPoint:result toView:nil];
64 }
65
66 CGRect ConvertRectToGlobal(SemanticsObject* reference, CGRect local_rect) {
67 SkM44 globalTransform = GetGlobalTransform(reference);
68
69 SkPoint quad[4] = {
70 SkPoint::Make(local_rect.origin.x, local_rect.origin.y), // top left
71 SkPoint::Make(local_rect.origin.x + local_rect.size.width, local_rect.origin.y), // top right
72 SkPoint::Make(local_rect.origin.x + local_rect.size.width,
73 local_rect.origin.y + local_rect.size.height), // bottom right
74 SkPoint::Make(local_rect.origin.x,
75 local_rect.origin.y + local_rect.size.height) // bottom left
76 };
77 for (auto& point : quad) {
78 point = ApplyTransform(point, globalTransform);
79 }
80 SkRect rect;
81 NSCAssert(rect.setBoundsCheck(quad, 4), @"Transformed points can't form a rect");
82 rect.setBounds(quad, 4);
83
84 // `rect` is in the physical pixel coordinate system. iOS expects the accessibility frame in
85 // the logical pixel coordinate system. Therefore, we divide by the `scale` (pixel ratio) to
86 // convert.
87 UIScreen* screen = reference.bridge->view().window.screen;
88 // Screen can be nil if the FlutterView is covered by another native view.
89 CGFloat scale = (screen ?: UIScreen.mainScreen).scale;
90 auto result =
91 CGRectMake(rect.x() / scale, rect.y() / scale, rect.width() / scale, rect.height() / scale);
92 return UIAccessibilityConvertFrameToScreenCoordinates(result, reference.bridge->view());
93 }
94
95 } // namespace
96
97 @interface FlutterSwitchSemanticsObject ()
98 @property(nonatomic, retain, readonly) UISwitch* nativeSwitch;
99 @end
100
101 @implementation FlutterSwitchSemanticsObject
102
103 - (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
104 uid:(int32_t)uid {
105 self = [super initWithBridge:bridge uid:uid];
106 if (self) {
107 _nativeSwitch = [[UISwitch alloc] init];
108 }
109 return self;
110 }
111
112 - (NSMethodSignature*)methodSignatureForSelector:(SEL)sel {
113 NSMethodSignature* result = [super methodSignatureForSelector:sel];
114 if (!result) {
115 result = [self.nativeSwitch methodSignatureForSelector:sel];
116 }
117 return result;
118 }
119
120 - (void)forwardInvocation:(NSInvocation*)anInvocation {
121 anInvocation.target = self.nativeSwitch;
122 [anInvocation invoke];
123 }
124
125 - (NSString*)accessibilityValue {
126 self.nativeSwitch.on = self.node.flags.isToggled || self.node.flags.isChecked;
127
128 if (![self isAccessibilityBridgeAlive]) {
129 return nil;
130 } else {
131 return self.nativeSwitch.accessibilityValue;
132 }
133 }
134
135 - (UIAccessibilityTraits)accessibilityTraits {
136 self.nativeSwitch.enabled = self.node.flags.isEnabled;
137
138 return self.nativeSwitch.accessibilityTraits;
139 }
140
141 @end // FlutterSwitchSemanticsObject
142
143 @interface FlutterScrollableSemanticsObject ()
144 @property(nonatomic) FlutterSemanticsScrollView* scrollView;
145 @end
146
147 @implementation FlutterScrollableSemanticsObject
148
149 - (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
150 uid:(int32_t)uid {
151 self = [super initWithBridge:bridge uid:uid];
152 if (self) {
153 _scrollView = [[FlutterSemanticsScrollView alloc] initWithSemanticsObject:self];
154 [_scrollView setShowsHorizontalScrollIndicator:NO];
155 [_scrollView setShowsVerticalScrollIndicator:NO];
156 [_scrollView setContentInset:UIEdgeInsetsZero];
157 [_scrollView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever];
158 [self.bridge->view() addSubview:_scrollView];
159 }
160 return self;
161 }
162
163 - (void)dealloc {
164 [_scrollView removeFromSuperview];
165 }
166
167 - (void)accessibilityBridgeDidFinishUpdate {
168 // In order to make iOS think this UIScrollView is scrollable, the following
169 // requirements must be true.
170 // 1. contentSize must be bigger than the frame size.
171 // 2. The scrollable isAccessibilityElement must return YES
172 //
173 // Once the requirements are met, the iOS uses contentOffset to determine
174 // what scroll actions are available. e.g. If the view scrolls vertically and
175 // contentOffset is 0.0, only the scroll down action is available.
176 self.scrollView.frame = self.accessibilityFrame;
177 self.scrollView.contentSize = [self contentSizeInternal];
178 // See the documentation on `isDoingSystemScrolling`.
179 if (!self.scrollView.isDoingSystemScrolling) {
180 [self.scrollView setContentOffset:self.contentOffsetInternal animated:NO];
181 }
182 }
183
184 - (id)nativeAccessibility {
185 return self.scrollView;
186 }
187
188 // private methods
189
190 - (float)scrollExtentMax {
191 if (![self isAccessibilityBridgeAlive]) {
192 return 0.0f;
193 }
194 float scrollExtentMax = self.node.scrollExtentMax;
195 if (isnan(scrollExtentMax)) {
196 scrollExtentMax = 0.0f;
197 } else if (!isfinite(scrollExtentMax)) {
198 scrollExtentMax = kScrollExtentMaxForInf + [self scrollPosition];
199 }
200 return scrollExtentMax;
201 }
202
203 - (float)scrollPosition {
204 if (![self isAccessibilityBridgeAlive]) {
205 return 0.0f;
206 }
207 float scrollPosition = self.node.scrollPosition;
208 if (isnan(scrollPosition)) {
209 scrollPosition = 0.0f;
210 }
211 NSCAssert(isfinite(scrollPosition), @"The scrollPosition must not be infinity");
212 return scrollPosition;
213 }
214
215 - (CGSize)contentSizeInternal {
216 CGRect result;
217 const SkRect& rect = self.node.rect;
218
219 if (self.node.actions & flutter::kVerticalScrollSemanticsActions) {
220 result = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height() + [self scrollExtentMax]);
221 } else if (self.node.actions & flutter::kHorizontalScrollSemanticsActions) {
222 result = CGRectMake(rect.x(), rect.y(), rect.width() + [self scrollExtentMax], rect.height());
223 } else {
224 result = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height());
225 }
226 return ConvertRectToGlobal(self, result).size;
227 }
228
229 - (CGPoint)contentOffsetInternal {
230 CGPoint result;
231 CGPoint origin = self.scrollView.frame.origin;
232 const SkRect& rect = self.node.rect;
233 if (self.node.actions & flutter::kVerticalScrollSemanticsActions) {
234 result = ConvertPointToGlobal(self, CGPointMake(rect.x(), rect.y() + [self scrollPosition]));
235 } else if (self.node.actions & flutter::kHorizontalScrollSemanticsActions) {
236 result = ConvertPointToGlobal(self, CGPointMake(rect.x() + [self scrollPosition], rect.y()));
237 } else {
238 result = origin;
239 }
240 return CGPointMake(result.x - origin.x, result.y - origin.y);
241 }
242
243 @end // FlutterScrollableSemanticsObject
244
245 @implementation FlutterCustomAccessibilityAction {
246 }
247 @end
248
249 @interface SemanticsObject ()
250 @property(nonatomic) SemanticsObjectContainer* container;
251
252 /** Should only be called in conjunction with setting child/parent relationship. */
253 @property(nonatomic, weak, readwrite) SemanticsObject* parent;
254
255 @end
256
257 @implementation SemanticsObject {
258 NSMutableArray<SemanticsObject*>* _children;
259 BOOL _inDealloc;
260 }
261
262 #pragma mark - Designated initializers
263
264 - (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
265 uid:(int32_t)uid {
266 FML_DCHECK(bridge) << "bridge must be set";
267 FML_DCHECK(uid >= kRootNodeId);
268 // Initialize with the UIView as the container.
269 // The UIView will not necessarily be accessibility parent for this object.
270 // The bridge informs the OS of the actual structure via
271 // `accessibilityContainer` and `accessibilityElementAtIndex`.
272 self = [super initWithAccessibilityContainer:bridge->view()];
273
274 if (self) {
275 _bridge = bridge;
276 _uid = uid;
277 _children = [[NSMutableArray alloc] init];
278 _childrenInHitTestOrder = [[NSArray alloc] init];
279 }
280
281 return self;
282 }
283
284 - (void)dealloc {
285 // Set parent and children parents to nil explicitly in dealloc.
286 // -[UIAccessibilityElement dealloc] has in the past called into -accessibilityContainer
287 // and self.children. There have also been crashes related to iOS
288 // accessing methods during dealloc, and there's a lag before the tree changes.
289 // See https://github.com/flutter/engine/pull/4602 and
290 // https://github.com/flutter/engine/pull/27786.
291 for (SemanticsObject* child in _children) {
292 child.parent = nil;
293 }
294 [_children removeAllObjects];
295
296 _parent = nil;
297 _inDealloc = YES;
298 }
299
300 #pragma mark - Semantic object property accesser
301
302 - (void)setChildren:(NSArray<SemanticsObject*>*)children {
303 for (SemanticsObject* child in _children) {
304 child.parent = nil;
305 }
306 _children = [children mutableCopy];
307 for (SemanticsObject* child in _children) {
308 child.parent = self;
309 }
310 }
311
312 - (void)setChildrenInHitTestOrder:(NSArray<SemanticsObject*>*)childrenInHitTestOrder {
313 for (SemanticsObject* child in _childrenInHitTestOrder) {
314 child.parent = nil;
315 }
316 _childrenInHitTestOrder = [childrenInHitTestOrder copy];
317 for (SemanticsObject* child in _childrenInHitTestOrder) {
318 child.parent = self;
319 }
320 }
321
322 - (BOOL)hasChildren {
323 return [self.children count] != 0;
324 }
325
326 #pragma mark - Semantic object method
327
328 - (BOOL)isAccessibilityBridgeAlive {
329 return self.bridge.get() != nil;
330 }
331
332 - (void)setSemanticsNode:(const flutter::SemanticsNode*)node {
333 _node = *node;
334 }
335
336 - (void)accessibilityBridgeDidFinishUpdate { /* Do nothing by default */
337 }
338
339 /**
340 * Whether calling `setSemanticsNode:` with `node` would cause a layout change.
341 */
342 - (BOOL)nodeWillCauseLayoutChange:(const flutter::SemanticsNode*)node {
343 return self.node.rect != node->rect || self.node.transform != node->transform;
344 }
345
346 /**
347 * Whether calling `setSemanticsNode:` with `node` would cause a scroll event.
348 */
349 - (BOOL)nodeWillCauseScroll:(const flutter::SemanticsNode*)node {
350 return !isnan(self.node.scrollPosition) && !isnan(node->scrollPosition) &&
351 self.node.scrollPosition != node->scrollPosition;
352 }
353
354 /**
355 * Whether calling `setSemanticsNode:` with `node` should trigger an
356 * announcement.
357 */
358 - (BOOL)nodeShouldTriggerAnnouncement:(const flutter::SemanticsNode*)node {
359 // The node dropped the live region flag, if it ever had one.
360 if (!node || !node->flags.isLiveRegion) {
361 return NO;
362 }
363
364 // The node has gained a new live region flag, always announce.
365 if (!self.node.flags.isLiveRegion) {
366 return YES;
367 }
368
369 // The label has updated, and the new node has a live region flag.
370 return self.node.label != node->label;
371 }
372
373 - (void)replaceChildAtIndex:(NSInteger)index withChild:(SemanticsObject*)child {
374 SemanticsObject* oldChild = _children[index];
375 oldChild.parent = nil;
376 child.parent = self;
377 [_children replaceObjectAtIndex:index withObject:child];
378 }
379
380 - (NSString*)routeName {
381 // Returns the first non-null and non-empty semantic label of a child
382 // with an NamesRoute flag. Otherwise returns nil.
383 if (self.node.flags.namesRoute) {
384 NSString* newName = self.accessibilityLabel;
385 if (newName != nil && [newName length] > 0) {
386 return newName;
387 }
388 }
389 if ([self hasChildren]) {
390 for (SemanticsObject* child in self.children) {
391 NSString* newName = [child routeName];
392 if (newName != nil && [newName length] > 0) {
393 return newName;
394 }
395 }
396 }
397 return nil;
398 }
399
400 - (id)nativeAccessibility {
401 return self;
402 }
403
404 - (NSAttributedString*)createAttributedStringFromString:(NSString*)string
405 withAttributes:
406 (const flutter::StringAttributes&)attributes {
407 NSMutableAttributedString* attributedString =
408 [[NSMutableAttributedString alloc] initWithString:string];
409 for (const auto& attribute : attributes) {
410 NSRange range = NSMakeRange(attribute->start, attribute->end - attribute->start);
411 switch (attribute->type) {
412 case flutter::StringAttributeType::kLocale: {
413 std::shared_ptr<flutter::LocaleStringAttribute> locale_attribute =
414 std::static_pointer_cast<flutter::LocaleStringAttribute>(attribute);
415 NSDictionary* attributeDict = @{
416 UIAccessibilitySpeechAttributeLanguage : @(locale_attribute->locale.data()),
417 };
418 [attributedString setAttributes:attributeDict range:range];
419 break;
420 }
421 case flutter::StringAttributeType::kSpellOut: {
422 NSDictionary* attributeDict = @{
423 UIAccessibilitySpeechAttributeSpellOut : @YES,
424 };
425 [attributedString setAttributes:attributeDict range:range];
426 break;
427 }
428 }
429 }
430 return attributedString;
431 }
432
433 - (void)showOnScreen {
434 self.bridge->DispatchSemanticsAction(self.uid, flutter::SemanticsAction::kShowOnScreen);
435 }
436
437 #pragma mark - UIAccessibility overrides
438
439 - (BOOL)isAccessibilityElement {
440 if (![self isAccessibilityBridgeAlive]) {
441 return false;
442 }
443
444 // Note: hit detection will only apply to elements that report
445 // -isAccessibilityElement of YES. The framework will continue scanning the
446 // entire element tree looking for such a hit.
447
448 // We enforce in the framework that no other useful semantics are merged with these nodes.
449 if (self.node.flags.scopesRoute) {
450 return false;
451 }
452
453 return [self isFocusable];
454 }
455
456 - (bool)isFocusable {
457 // If the node is scrollable AND hidden OR
458 // The node has a label, value, or hint OR
459 // The node has non-scrolling related actions.
460 //
461 // The kIsHidden flag set with the scrollable flag means this node is now
462 // hidden but still is a valid target for a11y focus in the tree, e.g. a list
463 // item that is currently off screen but the a11y navigation needs to know
464 // about.
465 return (self.node.flags.hasImplicitScrolling && self.node.flags.isHidden)
466
467 || !self.node.label.empty() || !self.node.value.empty() || !self.node.hint.empty() ||
468 (self.node.actions & ~flutter::kScrollableSemanticsActions) != 0;
469 }
470
471 - (void)collectRoutes:(NSMutableArray<SemanticsObject*>*)edges {
472 if (self.node.flags.scopesRoute) {
473 [edges addObject:self];
474 }
475 if ([self hasChildren]) {
476 for (SemanticsObject* child in self.children) {
477 [child collectRoutes:edges];
478 }
479 }
480 }
481
482 - (BOOL)onCustomAccessibilityAction:(FlutterCustomAccessibilityAction*)action {
483 if (!self.node.HasAction(flutter::SemanticsAction::kCustomAction)) {
484 return NO;
485 }
486 int32_t action_id = action.uid;
487 std::vector<uint8_t> args;
488 args.push_back(3); // type=int32.
489 args.push_back(action_id);
490 args.push_back(action_id >> 8);
491 args.push_back(action_id >> 16);
492 args.push_back(action_id >> 24);
493 self.bridge->DispatchSemanticsAction(
494 self.uid, flutter::SemanticsAction::kCustomAction,
495 fml::MallocMapping::Copy(args.data(), args.size() * sizeof(uint8_t)));
496 return YES;
497 }
498
499 - (NSString*)accessibilityIdentifier {
500 if (![self isAccessibilityBridgeAlive]) {
501 return nil;
502 }
503
504 if (self.node.identifier.empty()) {
505 return nil;
506 }
507 return @(self.node.identifier.data());
508 }
509
510 - (NSString*)accessibilityLabel {
511 if (![self isAccessibilityBridgeAlive]) {
512 return nil;
513 }
514 NSString* label = nil;
515 if (!self.node.label.empty()) {
516 label = @(self.node.label.data());
517 }
518 if (!self.node.tooltip.empty()) {
519 label = label ? [NSString stringWithFormat:@"%@\n%@", label, @(self.node.tooltip.data())]
520 : @(self.node.tooltip.data());
521 }
522 return label;
523 }
524
525 - (bool)containsPoint:(CGPoint)point {
526 // The point is in global coordinates, so use the global rect here.
527 return CGRectContainsPoint([self globalRect], point);
528 }
529
530 // Finds the first eligiable semantics object in hit test order.
531 - (id)search:(CGPoint)point {
532 // Search children in hit test order.
533 for (SemanticsObject* child in [self childrenInHitTestOrder]) {
534 if ([child containsPoint:point]) {
535 id childSearchResult = [child search:point];
536 if (childSearchResult != nil) {
537 return childSearchResult;
538 }
539 }
540 }
541 // Check if the current semantic object should be returned.
542 if ([self containsPoint:point] && [self isFocusable]) {
543 return self.nativeAccessibility;
544 }
545 return nil;
546 }
547
548 // iOS uses this method to determine the hittest results when users touch
549 // explore in VoiceOver.
550 //
551 // For overlapping UIAccessibilityElements (e.g. a stack) in IOS, the focus
552 // goes to the smallest object before IOS 16, but to the top-left object in
553 // IOS 16. Overrides this method to focus the first eligiable semantics
554 // object in hit test order.
555 - (id)_accessibilityHitTest:(CGPoint)point withEvent:(UIEvent*)event {
556 return [self search:point];
557 }
558
559 // iOS calls this method when this item is swipe-to-focusd in VoiceOver.
560 - (BOOL)accessibilityScrollToVisible {
561 [self showOnScreen];
562 return YES;
563 }
564
565 // iOS calls this method when this item is swipe-to-focusd in VoiceOver.
566 - (BOOL)accessibilityScrollToVisibleWithChild:(id)child {
567 if ([child isKindOfClass:[SemanticsObject class]]) {
568 [child showOnScreen];
569 return YES;
570 }
571 return NO;
572 }
573
574 - (NSAttributedString*)accessibilityAttributedLabel {
575 NSString* label = self.accessibilityLabel;
576 if (label.length == 0) {
577 return nil;
578 }
579 return [self createAttributedStringFromString:label withAttributes:self.node.labelAttributes];
580 }
581
582 - (NSString*)accessibilityHint {
583 if (![self isAccessibilityBridgeAlive]) {
584 return nil;
585 }
586
587 if (self.node.hint.empty()) {
588 return nil;
589 }
590 return @(self.node.hint.data());
591 }
592
593 - (NSAttributedString*)accessibilityAttributedHint {
594 NSString* hint = [self accessibilityHint];
595 if (hint.length == 0) {
596 return nil;
597 }
598 return [self createAttributedStringFromString:hint withAttributes:self.node.hintAttributes];
599 }
600
601 - (NSString*)accessibilityValue {
602 if (![self isAccessibilityBridgeAlive]) {
603 return nil;
604 }
605
606 if (!self.node.value.empty()) {
607 return @(self.node.value.data());
608 }
609
610 // iOS does not announce values of native radio buttons.
611 if (self.node.flags.isInMutuallyExclusiveGroup) {
612 return nil;
613 }
614
615 // FlutterSwitchSemanticsObject should supercede these conditionals.
616 if (self.node.flags.hasToggledState || self.node.flags.hasCheckedState) {
617 if (self.node.flags.isToggled || self.node.flags.isChecked) {
618 return @"1";
619 } else {
620 return @"0";
621 }
622 }
623
624 return nil;
625 }
626
627 - (NSAttributedString*)accessibilityAttributedValue {
628 NSString* value = [self accessibilityValue];
629 if (value.length == 0) {
630 return nil;
631 }
632 return [self createAttributedStringFromString:value withAttributes:self.node.valueAttributes];
633 }
634
635 - (CGRect)accessibilityFrame {
636 if (![self isAccessibilityBridgeAlive]) {
637 return CGRectMake(0, 0, 0, 0);
638 }
639
640 if (self.node.flags.isHidden) {
641 return [super accessibilityFrame];
642 }
643 return [self globalRect];
644 }
645
646 - (CGRect)globalRect {
647 const SkRect& rect = self.node.rect;
648 CGRect localRect = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height());
649 return ConvertRectToGlobal(self, localRect);
650 }
651
652 #pragma mark - UIAccessibilityElement protocol
653
654 - (void)setAccessibilityContainer:(id)container {
655 // Explicit noop. The containers are calculated lazily in `accessibilityContainer`.
656 // See also: https://github.com/flutter/flutter/issues/54366
657 }
658
659 - (id)accessibilityContainer {
660 if (_inDealloc) {
661 // In iOS9, `accessibilityContainer` will be called by `[UIAccessibilityElementSuperCategory
662 // dealloc]` during `[super dealloc]`. And will crash when accessing `_children` which has
663 // called `[_children release]` in `[SemanticsObject dealloc]`.
664 // https://github.com/flutter/flutter/issues/87247
665 return nil;
666 }
667
668 if (![self isAccessibilityBridgeAlive]) {
669 return nil;
670 }
671
672 if ([self hasChildren] || self.uid == kRootNodeId) {
673 if (self.container == nil) {
674 self.container = [[SemanticsObjectContainer alloc] initWithSemanticsObject:self
675 bridge:self.bridge];
676 }
677 return self.container;
678 }
679 if (self.parent == nil) {
680 // This can happen when we have released the accessibility tree but iOS is
681 // still holding onto our objects. iOS can take some time before it
682 // realizes that the tree has changed.
683 return nil;
684 }
685 return self.parent.accessibilityContainer;
686 }
687
688 #pragma mark - UIAccessibilityAction overrides
689
690 - (BOOL)accessibilityActivate {
691 if (![self isAccessibilityBridgeAlive]) {
692 return NO;
693 }
694 if (!self.node.HasAction(flutter::SemanticsAction::kTap)) {
695 // Prevent sliders to receive a regular tap which will change the value.
696 //
697 // This is needed because it causes slider to select to middle if it
698 // does not have a semantics tap.
699 if (self.node.flags.isSlider) {
700 return YES;
701 }
702 return NO;
703 }
704 self.bridge->DispatchSemanticsAction(self.uid, flutter::SemanticsAction::kTap);
705 return YES;
706 }
707
708 - (void)accessibilityIncrement {
709 if (![self isAccessibilityBridgeAlive]) {
710 return;
711 }
712 if (self.node.HasAction(flutter::SemanticsAction::kIncrease)) {
713 self.node.value = self.node.increasedValue;
714 self.bridge->DispatchSemanticsAction(self.uid, flutter::SemanticsAction::kIncrease);
715 }
716 }
717
718 - (void)accessibilityDecrement {
719 if (![self isAccessibilityBridgeAlive]) {
720 return;
721 }
722 if (self.node.HasAction(flutter::SemanticsAction::kDecrease)) {
723 self.node.value = self.node.decreasedValue;
724 self.bridge->DispatchSemanticsAction(self.uid, flutter::SemanticsAction::kDecrease);
725 }
726 }
727
728 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
729 if (![self isAccessibilityBridgeAlive]) {
730 return NO;
731 }
732 flutter::SemanticsAction action = GetSemanticsActionForScrollDirection(direction);
733 if (!self.node.HasAction(action)) {
734 return NO;
735 }
736 self.bridge->DispatchSemanticsAction(self.uid, action);
737 return YES;
738 }
739
740 - (BOOL)accessibilityPerformEscape {
741 if (![self isAccessibilityBridgeAlive]) {
742 return NO;
743 }
744 if (!self.node.HasAction(flutter::SemanticsAction::kDismiss)) {
745 return NO;
746 }
747 self.bridge->DispatchSemanticsAction(self.uid, flutter::SemanticsAction::kDismiss);
748 return YES;
749 }
750
751 #pragma mark UIAccessibilityFocus overrides
752
753 - (void)accessibilityElementDidBecomeFocused {
754 if (![self isAccessibilityBridgeAlive]) {
755 return;
756 }
757 self.bridge->AccessibilityObjectDidBecomeFocused(self.uid);
758 if (self.node.flags.isHidden || self.node.flags.isHeader) {
759 [self showOnScreen];
760 }
761 if (self.node.HasAction(flutter::SemanticsAction::kDidGainAccessibilityFocus)) {
762 self.bridge->DispatchSemanticsAction(self.uid,
763 flutter::SemanticsAction::kDidGainAccessibilityFocus);
764 }
765 }
766
767 - (void)accessibilityElementDidLoseFocus {
768 if (![self isAccessibilityBridgeAlive]) {
769 return;
770 }
771 self.bridge->AccessibilityObjectDidLoseFocus(self.uid);
772 if (self.node.HasAction(flutter::SemanticsAction::kDidLoseAccessibilityFocus)) {
773 self.bridge->DispatchSemanticsAction(self.uid,
774 flutter::SemanticsAction::kDidLoseAccessibilityFocus);
775 }
776 }
777
778 - (BOOL)accessibilityRespondsToUserInteraction {
779 // Return true only if the node contains actions other than system actions.
780 if ((self.node.actions & ~flutter::kSystemActions) != 0) {
781 return true;
782 }
783
784 if (!self.node.customAccessibilityActions.empty()) {
785 return true;
786 }
787
788 return false;
789 }
790
791 @end
792
793 @implementation FlutterSemanticsObject
794
795 #pragma mark - Designated initializers
796
797 - (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
798 uid:(int32_t)uid {
799 self = [super initWithBridge:bridge uid:uid];
800 return self;
801 }
802
803 #pragma mark - UIAccessibility overrides
804
805 - (UIAccessibilityTraits)accessibilityTraits {
806 UIAccessibilityTraits traits = UIAccessibilityTraitNone;
807 if (self.node.HasAction(flutter::SemanticsAction::kIncrease) ||
808 self.node.HasAction(flutter::SemanticsAction::kDecrease)) {
809 traits |= UIAccessibilityTraitAdjustable;
810 }
811 // This should also capture radio buttons.
812 if (self.node.flags.hasToggledState || self.node.flags.hasCheckedState) {
813 traits |= UIAccessibilityTraitButton;
814 }
815 if (self.node.flags.isSelected) {
816 traits |= UIAccessibilityTraitSelected;
817 }
818 if (self.node.flags.isButton) {
819 traits |= UIAccessibilityTraitButton;
820 }
821 if (self.node.flags.hasEnabledState && !self.node.flags.isEnabled) {
822 traits |= UIAccessibilityTraitNotEnabled;
823 }
824 if (self.node.flags.isHeader) {
825 traits |= UIAccessibilityTraitHeader;
826 }
827 if (self.node.flags.isImage) {
828 traits |= UIAccessibilityTraitImage;
829 }
830 if (self.node.flags.isLiveRegion) {
831 traits |= UIAccessibilityTraitUpdatesFrequently;
832 }
833 if (self.node.flags.isLink) {
834 traits |= UIAccessibilityTraitLink;
835 }
836 if (traits == UIAccessibilityTraitNone && ![self hasChildren] &&
837 self.accessibilityLabel.length != 0 && !self.node.flags.isTextField) {
838 traits = UIAccessibilityTraitStaticText;
839 }
840 return traits;
841 }
842
843 @end
844
845 @interface FlutterPlatformViewSemanticsContainer ()
846 @property(nonatomic, weak) UIView* platformView;
847 @end
848
849 @implementation FlutterPlatformViewSemanticsContainer
850
851 - (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
852 uid:(int32_t)uid
853 platformView:(nonnull FlutterTouchInterceptingView*)platformView {
854 if (self = [super initWithBridge:bridge uid:uid]) {
855 _platformView = platformView;
856 [platformView setFlutterAccessibilityContainer:self];
857 }
858 return self;
859 }
860
861 - (id)nativeAccessibility {
862 return self.platformView;
863 }
864
865 @end
866
867 @implementation SemanticsObjectContainer {
868 fml::WeakPtr<flutter::AccessibilityBridgeIos> _bridge;
869 }
870
871 #pragma mark - initializers
872
873 - (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject
874 bridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge {
875 FML_DCHECK(semanticsObject) << "semanticsObject must be set";
876 // Initialize with the UIView as the container.
877 // The UIView will not necessarily be accessibility parent for this object.
878 // The bridge informs the OS of the actual structure via
879 // `accessibilityContainer` and `accessibilityElementAtIndex`.
880 self = [super initWithAccessibilityContainer:bridge->view()];
881
882 if (self) {
883 _semanticsObject = semanticsObject;
884 _bridge = bridge;
885 }
886
887 return self;
888 }
889
890 #pragma mark - UIAccessibilityContainer overrides
891
892 - (NSInteger)accessibilityElementCount {
893 return self.semanticsObject.children.count + 1;
894 }
895
896 - (nullable id)accessibilityElementAtIndex:(NSInteger)index {
897 if (index < 0 || index >= [self accessibilityElementCount]) {
898 return nil;
899 }
900 if (index == 0) {
901 return self.semanticsObject.nativeAccessibility;
902 }
903
904 SemanticsObject* child = self.semanticsObject.children[index - 1];
905
906 if ([child hasChildren]) {
907 return child.accessibilityContainer;
908 }
909 return child.nativeAccessibility;
910 }
911
912 - (NSInteger)indexOfAccessibilityElement:(id)element {
913 if (element == self.semanticsObject.nativeAccessibility) {
914 return 0;
915 }
916
917 NSArray<SemanticsObject*>* children = self.semanticsObject.children;
918 for (size_t i = 0; i < [children count]; i++) {
919 SemanticsObject* child = children[i];
920 if ((![child hasChildren] && child.nativeAccessibility == element) ||
921 ([child hasChildren] && [child.nativeAccessibility accessibilityContainer] == element)) {
922 return i + 1;
923 }
924 }
925 return NSNotFound;
926 }
927
928 #pragma mark - UIAccessibilityElement protocol
929
930 - (BOOL)isAccessibilityElement {
931 return NO;
932 }
933
934 - (CGRect)accessibilityFrame {
935 // For OverlayPortals, the child element is sometimes outside the bounds of the parent
936 // Even if it's marked accessible, VoiceControl labels will not appear if it's too
937 // spatially distant. Set the frame to be the max screen size so all children are guaraenteed
938 // to be contained.
939
940 return UIScreen.mainScreen.bounds;
941 }
942
943 - (id)accessibilityContainer {
944 if (!_bridge) {
945 return nil;
946 }
947 return ([self.semanticsObject uid] == kRootNodeId)
948 ? _bridge->view()
949 : self.semanticsObject.parent.accessibilityContainer;
950 }
951
952 #pragma mark - UIAccessibilityAction overrides
953
954 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
955 return [self.semanticsObject accessibilityScroll:direction];
956 }
957
958 @end
FlutterPlatformViews_Internal.h
FlutterSemanticsScrollView.h
self
return self
Definition: FlutterTextureRegistryRelay.mm:19
SemanticsObject.h
kRootNodeId
constexpr int32_t kRootNodeId
Definition: SemanticsObject.h:16
kScrollExtentMaxForInf
constexpr float kScrollExtentMaxForInf
Definition: SemanticsObject.h:18
_inDealloc
BOOL _inDealloc
Definition: SemanticsObject.mm:257
FlutterTouchInterceptingView
Definition: FlutterPlatformViews.mm:513
FlutterCustomAccessibilityAction
Definition: SemanticsObject.h:136
FlutterCustomAccessibilityAction::uid
int32_t uid
Definition: SemanticsObject.h:140
FlutterPlatformViewSemanticsContainer
Definition: SemanticsObject.h:170
FlutterScrollableSemanticsObject
Definition: SemanticsObject.h:190
FlutterScrollableSemanticsObject::scrollView
FlutterSemanticsScrollView * scrollView
Definition: SemanticsObject.h:190
FlutterSemanticsObject
Definition: SemanticsObject.h:156
FlutterSemanticsScrollView
Definition: FlutterSemanticsScrollView.h:21
FlutterSemanticsScrollView::isDoingSystemScrolling
BOOL isDoingSystemScrolling
Definition: FlutterSemanticsScrollView.h:43
FlutterSwitchSemanticsObject
Definition: SemanticsObject.h:184
SemanticsObjectContainer
Definition: SemanticsObject.h:228
SemanticsObject
Definition: SemanticsObject.h:32
SemanticsObject::parent
SemanticsObject * parent
Definition: SemanticsObject.h:42
SemanticsObject::nativeAccessibility
id nativeAccessibility
Definition: SemanticsObject.h:83
-[SemanticsObject isAccessibilityBridgeAlive]
BOOL isAccessibilityBridgeAlive()
Definition: SemanticsObject.mm:328
-[SemanticsObject accessibilityBridgeDidFinishUpdate]
void accessibilityBridgeDidFinishUpdate()
Definition: SemanticsObject.mm:336
SemanticsObject::uid
int32_t uid
Definition: SemanticsObject.h:36
SemanticsObject::hasChildren
BOOL hasChildren
Definition: SemanticsObject.h:62
SemanticsObject::node
flutter::SemanticsNode node
Definition: SemanticsObject.h:57
SemanticsObject::children
NSArray< SemanticsObject * > * children
Definition: SemanticsObject.h:68
SemanticsObject::bridge
fml::WeakPtr< flutter::AccessibilityBridgeIos > bridge
Definition: SemanticsObject.h:52
FLUTTER_ASSERT_ARC
Definition: FlutterChannelKeyResponder.mm:13
FLUTTER_ASSERT_ARC::ConvertRectToGlobal
CGRect ConvertRectToGlobal(SemanticsObject *reference, CGRect local_rect)
Definition: SemanticsObject.mm:66
FLUTTER_ASSERT_ARC::GetSemanticsActionForScrollDirection
flutter::SemanticsAction GetSemanticsActionForScrollDirection(UIAccessibilityScrollDirection direction)
Definition: SemanticsObject.mm:14
FLUTTER_ASSERT_ARC::ConvertPointToGlobal
CGPoint ConvertPointToGlobal(SemanticsObject *reference, CGPoint local_point)
Definition: SemanticsObject.mm:52
FLUTTER_ASSERT_ARC::GetGlobalTransform
SkM44 GetGlobalTransform(SemanticsObject *reference)
Definition: SemanticsObject.mm:39
FLUTTER_ASSERT_ARC::ApplyTransform
SkPoint ApplyTransform(SkPoint &point, const SkM44 &transform)
Definition: SemanticsObject.mm:47
flutter
Definition: accessibility_bridge.h:26
fml
Definition: profiler_metrics_ios.mm:41