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