| 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*)accessibilityLabel {
528 if (![self isAccessibilityBridgeAlive]) {
529 return nil;
530 }
531 NSString* label = nil;
532 if (![self node].label.empty()) {
533 label = @([self node].label.data());
534 }
535 if (![self node].tooltip.empty()) {
536 label = label ? [NSString stringWithFormat:@"%@\n%@", label, @([self node].tooltip.data())]
537 : @([self node].tooltip.data());
538 }
539 return label;
540 }
541
542 - (bool)containsPoint:(CGPoint)point {
543 // The point is in global coordinates, so use the global rect here.
544 return CGRectContainsPoint([self globalRect], point);
545 }
546
547 // Finds the first eligiable semantics object in hit test order.
548 - (id)search:(CGPoint)point {
549 // Search children in hit test order.
550 for (SemanticsObject* child in [self childrenInHitTestOrder]) {
551 if ([child containsPoint:point]) {
552 id childSearchResult = [child search:point];
553 if (childSearchResult != nil) {
554 return childSearchResult;
555 }
556 }
557 }
558 // Check if the current semantic object should be returned.
559 if ([self containsPoint:point] && [self isFocusable]) {
560 return self.nativeAccessibility;
561 }
562 return nil;
563 }
564
565 // iOS uses this method to determine the hittest results when users touch
566 // explore in VoiceOver.
567 //
568 // For overlapping UIAccessibilityElements (e.g. a stack) in IOS, the focus
569 // goes to the smallest object before IOS 16, but to the top-left object in
570 // IOS 16. Overrides this method to focus the first eligiable semantics
571 // object in hit test order.
572 - (id)_accessibilityHitTest:(CGPoint)point withEvent:(UIEvent*)event {
573 return [self search:point];
574 }
575
576 // iOS calls this method when this item is swipe-to-focusd in VoiceOver.
577 - (BOOL)accessibilityScrollToVisible {
578 [self showOnScreen];
579 return YES;
580 }
581
582 // iOS calls this method when this item is swipe-to-focusd in VoiceOver.
583 - (BOOL)accessibilityScrollToVisibleWithChild:(id)child {
584 if ([child isKindOfClass:[SemanticsObject class]]) {
585 [child showOnScreen];
586 return YES;
587 }
588 return NO;
589 }
590
591 - (NSAttributedString*)accessibilityAttributedLabel {
592 NSString* label = [self accessibilityLabel];
593 if (label.length == 0) {
594 return nil;
595 }
596 return [self createAttributedStringFromString:label withAttributes:[self node].labelAttributes];
597 }
598
599 - (NSString*)accessibilityHint {
600 if (![self isAccessibilityBridgeAlive]) {
601 return nil;
602 }
603
604 if ([self node].hint.empty()) {
605 return nil;
606 }
607 return @([self node].hint.data());
608 }
609
610 - (NSAttributedString*)accessibilityAttributedHint {
611 NSString* hint = [self accessibilityHint];
612 if (hint.length == 0) {
613 return nil;
614 }
615 return [self createAttributedStringFromString:hint withAttributes:[self node].hintAttributes];
616 }
617
618 - (NSString*)accessibilityValue {
619 if (![self isAccessibilityBridgeAlive]) {
620 return nil;
621 }
622
623 if (![self node].value.empty()) {
624 return @([self node].value.data());
625 }
626
627 // iOS does not announce values of native radio buttons.
628 if ([self node].HasFlag(flutter::SemanticsFlags::kIsInMutuallyExclusiveGroup)) {
629 return nil;
630 }
631
632 // FlutterSwitchSemanticsObject should supercede these conditionals.
633 if ([self node].HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
634 [self node].HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
635 if ([self node].HasFlag(flutter::SemanticsFlags::kIsToggled) ||
636 [self node].HasFlag(flutter::SemanticsFlags::kIsChecked)) {
637 return @"1";
638 } else {
639 return @"0";
640 }
641 }
642
643 return nil;
644 }
645
646 - (NSAttributedString*)accessibilityAttributedValue {
647 NSString* value = [self accessibilityValue];
648 if (value.length == 0) {
649 return nil;
650 }
651 return [self createAttributedStringFromString:value withAttributes:[self node].valueAttributes];
652 }
653
654 - (CGRect)accessibilityFrame {
655 if (![self isAccessibilityBridgeAlive]) {
656 return CGRectMake(0, 0, 0, 0);
657 }
658
659 if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden)) {
660 return [super accessibilityFrame];
661 }
662 return [self globalRect];
663 }
664
665 - (CGRect)globalRect {
666 const SkRect& rect = [self node].rect;
667 CGRect localRect = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height());
668 return ConvertRectToGlobal(self, localRect);
669 }
670
671 #pragma mark - UIAccessibilityElement protocol
672
673 - (void)setAccessibilityContainer:(id)container {
674 // Explicit noop. The containers are calculated lazily in `accessibilityContainer`.
675 // See also: https://github.com/flutter/flutter/issues/54366
676 }
677
678 - (id)accessibilityContainer {
679 if (_inDealloc) {
680 // In iOS9, `accessibilityContainer` will be called by `[UIAccessibilityElementSuperCategory
681 // dealloc]` during `[super dealloc]`. And will crash when accessing `_children` which has
682 // called `[_children release]` in `[SemanticsObject dealloc]`.
683 // https://github.com/flutter/flutter/issues/87247
684 return nil;
685 }
686
687 if (![self isAccessibilityBridgeAlive]) {
688 return nil;
689 }
690
691 if ([self hasChildren] || [self uid] == kRootNodeId) {
692 if (_container == nil) {
693 _container.reset([[SemanticsObjectContainer alloc] initWithSemanticsObject:self
694 bridge:[self bridge]]);
695 }
696 return _container.get();
697 }
698 if ([self parent] == nil) {
699 // This can happen when we have released the accessibility tree but iOS is
700 // still holding onto our objects. iOS can take some time before it
701 // realizes that the tree has changed.
702 return nil;
703 }
704 return [[self parent] accessibilityContainer];
705 }
706
707 #pragma mark - UIAccessibilityAction overrides
708
709 - (BOOL)accessibilityActivate {
710 if (![self isAccessibilityBridgeAlive]) {
711 return NO;
712 }
713 if (![self node].HasAction(flutter::SemanticsAction::kTap)) {
714 return NO;
715 }
716 [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kTap);
717 return YES;
718 }
719
720 - (void)accessibilityIncrement {
721 if (![self isAccessibilityBridgeAlive]) {
722 return;
723 }
724 if ([self node].HasAction(flutter::SemanticsAction::kIncrease)) {
725 [self node].value = [self node].increasedValue;
726 [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kIncrease);
727 }
728 }
729
730 - (void)accessibilityDecrement {
731 if (![self isAccessibilityBridgeAlive]) {
732 return;
733 }
734 if ([self node].HasAction(flutter::SemanticsAction::kDecrease)) {
735 [self node].value = [self node].decreasedValue;
736 [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kDecrease);
737 }
738 }
739
740 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
741 if (![self isAccessibilityBridgeAlive]) {
742 return NO;
743 }
744 flutter::SemanticsAction action = GetSemanticsActionForScrollDirection(direction);
745 if (![self node].HasAction(action)) {
746 return NO;
747 }
748 [self bridge]->DispatchSemanticsAction([self uid], action);
749 return YES;
750 }
751
752 - (BOOL)accessibilityPerformEscape {
753 if (![self isAccessibilityBridgeAlive]) {
754 return NO;
755 }
756 if (![self node].HasAction(flutter::SemanticsAction::kDismiss)) {
757 return NO;
758 }
759 [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kDismiss);
760 return YES;
761 }
762
763 #pragma mark UIAccessibilityFocus overrides
764
765 - (void)accessibilityElementDidBecomeFocused {
766 if (![self isAccessibilityBridgeAlive]) {
767 return;
768 }
769 [self bridge]->AccessibilityObjectDidBecomeFocused([self uid]);
770 if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden) ||
771 [self node].HasFlag(flutter::SemanticsFlags::kIsHeader)) {
772 [self showOnScreen];
773 }
774 if ([self node].HasAction(flutter::SemanticsAction::kDidGainAccessibilityFocus)) {
775 [self bridge]->DispatchSemanticsAction([self uid],
776 flutter::SemanticsAction::kDidGainAccessibilityFocus);
777 }
778 }
779
780 - (void)accessibilityElementDidLoseFocus {
781 if (![self isAccessibilityBridgeAlive]) {
782 return;
783 }
784 [self bridge]->AccessibilityObjectDidLoseFocus([self uid]);
785 if ([self node].HasAction(flutter::SemanticsAction::kDidLoseAccessibilityFocus)) {
786 [self bridge]->DispatchSemanticsAction([self uid],
787 flutter::SemanticsAction::kDidLoseAccessibilityFocus);
788 }
789 }
790
791 @end
792
793 @implementation FlutterSemanticsObject {
794 }
795
796 #pragma mark - Override base class designated initializers
797
798 // Method declared as unavailable in the interface
799 - (instancetype)init {
800 [self release];
801 [super doesNotRecognizeSelector:_cmd];
802 return nil;
803 }
804
805 #pragma mark - Designated initializers
806
807 - (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
808 uid:(int32_t)uid {
809 self = [super initWithBridge:bridge uid:uid];
810 return self;
811 }
812
813 #pragma mark - UIAccessibility overrides
814
815 - (UIAccessibilityTraits)accessibilityTraits {
816 UIAccessibilityTraits traits = UIAccessibilityTraitNone;
817 if ([self node].HasAction(flutter::SemanticsAction::kIncrease) ||
818 [self node].HasAction(flutter::SemanticsAction::kDecrease)) {
819 traits |= UIAccessibilityTraitAdjustable;
820 }
821 // This should also capture radio buttons.
822 if ([self node].HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
823 [self node].HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
824 traits |= UIAccessibilityTraitButton;
825 }
826 if ([self node].HasFlag(flutter::SemanticsFlags::kIsSelected)) {
827 traits |= UIAccessibilityTraitSelected;
828 }
829 if ([self node].HasFlag(flutter::SemanticsFlags::kIsButton)) {
830 traits |= UIAccessibilityTraitButton;
831 }
832 if ([self node].HasFlag(flutter::SemanticsFlags::kHasEnabledState) &&
833 ![self node].HasFlag(flutter::SemanticsFlags::kIsEnabled)) {
834 traits |= UIAccessibilityTraitNotEnabled;
835 }
836 if ([self node].HasFlag(flutter::SemanticsFlags::kIsHeader)) {
837 traits |= UIAccessibilityTraitHeader;
838 }
839 if ([self node].HasFlag(flutter::SemanticsFlags::kIsImage)) {
840 traits |= UIAccessibilityTraitImage;
841 }
842 if ([self node].HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
843 traits |= UIAccessibilityTraitUpdatesFrequently;
844 }
845 if ([self node].HasFlag(flutter::SemanticsFlags::kIsLink)) {
846 traits |= UIAccessibilityTraitLink;
847 }
848 if (traits == UIAccessibilityTraitNone && ![self hasChildren] &&
849 [[self accessibilityLabel] length] != 0 &&
850 ![self node].HasFlag(flutter::SemanticsFlags::kIsTextField)) {
851 traits = UIAccessibilityTraitStaticText;
852 }
853 return traits;
854 }
855
856 @end
857
858 @interface FlutterPlatformViewSemanticsContainer ()
859 @property(nonatomic, retain) UIView* platformView;
860 @end
861
862 @implementation FlutterPlatformViewSemanticsContainer
863
864 - (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
865 uid:(int32_t)uid
866 platformView:(nonnull FlutterTouchInterceptingView*)platformView {
867 if (self = [super initWithBridge:bridge uid:uid]) {
868 _platformView = [platformView retain];
869 [platformView setFlutterAccessibilityContainer:self];
870 }
871 return self;
872 }
873
874 - (void)dealloc {
875 [_platformView release];
876 _platformView = nil;
877 [super dealloc];
878 }
879
880 - (id)nativeAccessibility {
881 return _platformView;
882 }
883
884 @end
885
886 @implementation SemanticsObjectContainer {
887 SemanticsObject* _semanticsObject;
888 fml::WeakPtr<flutter::AccessibilityBridgeIos> _bridge;
889 }
890
891 #pragma mark - initializers
892
893 // Method declared as unavailable in the interface
894 - (instancetype)init {
895 [self release];
896 [super doesNotRecognizeSelector:_cmd];
897 return nil;
898 }
899
900 - (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject
901 bridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge {
902 FML_DCHECK(semanticsObject) << "semanticsObject must be set";
903 // Initialize with the UIView as the container.
904 // The UIView will not necessarily be accessibility parent for this object.
905 // The bridge informs the OS of the actual structure via
906 // `accessibilityContainer` and `accessibilityElementAtIndex`.
907 self = [super initWithAccessibilityContainer:bridge->view()];
908
909 if (self) {
910 _semanticsObject = semanticsObject;
911 _bridge = bridge;
912 }
913
914 return self;
915 }
916
917 #pragma mark - UIAccessibilityContainer overrides
918
919 - (NSInteger)accessibilityElementCount {
920 NSInteger count = [[_semanticsObject children] count] + 1;
921 return count;
922 }
923
924 - (nullable id)accessibilityElementAtIndex:(NSInteger)index {
925 if (index < 0 || index >= [self accessibilityElementCount]) {
926 return nil;
927 }
928 if (index == 0) {
929 return _semanticsObject.nativeAccessibility;
930 }
931
932 SemanticsObject* child = [_semanticsObject children][index - 1];
933
934 if ([child hasChildren]) {
935 return [child accessibilityContainer];
936 }
937 return child.nativeAccessibility;
938 }
939
940 - (NSInteger)indexOfAccessibilityElement:(id)element {
941 if (element == _semanticsObject.nativeAccessibility) {
942 return 0;
943 }
944
945 NSArray<SemanticsObject*>* children = [_semanticsObject children];
946 for (size_t i = 0; i < [children count]; i++) {
947 SemanticsObject* child = children[i];
948 if ((![child hasChildren] && child.nativeAccessibility == element) ||
949 ([child hasChildren] && [child.nativeAccessibility accessibilityContainer] == element)) {
950 return i + 1;
951 }
952 }
953 return NSNotFound;
954 }
955
956 #pragma mark - UIAccessibilityElement protocol
957
958 - (BOOL)isAccessibilityElement {
959 return NO;
960 }
961
962 - (CGRect)accessibilityFrame {
963 return [_semanticsObject accessibilityFrame];
964 }
965
966 - (id)accessibilityContainer {
967 if (!_bridge) {
968 return nil;
969 }
970 return ([_semanticsObject uid] == kRootNodeId)
971 ? _bridge->view()
972 : [[_semanticsObject parent] accessibilityContainer];
973 }
974
975 #pragma mark - UIAccessibilityAction overrides
976
977 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
978 return [_semanticsObject accessibilityScroll:direction];
979 }
980
981 @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
action
SemanticsAction action
Definition: SemanticsObjectTestMocks.h:21
SemanticsObject::bridge
fml::WeakPtr< flutter::AccessibilityBridgeIos > bridge
Definition: SemanticsObject.h:51
_bridge
fml::WeakPtr< flutter::AccessibilityBridgeIos > _bridge
Definition: SemanticsObject.mm:886
FlutterSemanticsScrollView
Definition: FlutterSemanticsScrollView.h:21
-[FlutterTouchInterceptingView setFlutterAccessibilityContainer:]
void setFlutterAccessibilityContainer:(NSObject *flutterAccessibilityContainer)
Definition: FlutterPlatformViews.mm:1047
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:961
SemanticsObject::uid
int32_t uid
Definition: SemanticsObject.h:35
-[SemanticsObject accessibilityBridgeDidFinishUpdate]
void accessibilityBridgeDidFinishUpdate()
Definition: SemanticsObject.mm:356
SemanticsObjectContainer
Definition: SemanticsObject.h:226
id
int32_t id
Definition: SemanticsObjectTestMocks.h:20
_scrollView
fml::scoped_nsobject< UIScrollView > _scrollView
Definition: FlutterViewController.mm:134
FlutterScrollableSemanticsObject
Definition: SemanticsObject.h:188
SemanticsObject
Definition: SemanticsObject.h:30