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