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