Flutter iOS Embedder
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros
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.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 
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 
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`.
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 @end
785 
786 @implementation FlutterSemanticsObject
787 
788 #pragma mark - Designated initializers
789 
790 - (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
791  uid:(int32_t)uid {
792  self = [super initWithBridge:bridge uid:uid];
793  return self;
794 }
795 
796 #pragma mark - UIAccessibility overrides
797 
798 - (UIAccessibilityTraits)accessibilityTraits {
799  UIAccessibilityTraits traits = UIAccessibilityTraitNone;
800  if (self.node.HasAction(flutter::SemanticsAction::kIncrease) ||
801  self.node.HasAction(flutter::SemanticsAction::kDecrease)) {
802  traits |= UIAccessibilityTraitAdjustable;
803  }
804  // This should also capture radio buttons.
805  if (self.node.HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
806  self.node.HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
807  traits |= UIAccessibilityTraitButton;
808  }
809  if (self.node.HasFlag(flutter::SemanticsFlags::kIsSelected)) {
810  traits |= UIAccessibilityTraitSelected;
811  }
812  if (self.node.HasFlag(flutter::SemanticsFlags::kIsButton)) {
813  traits |= UIAccessibilityTraitButton;
814  }
815  if (self.node.HasFlag(flutter::SemanticsFlags::kHasEnabledState) &&
816  !self.node.HasFlag(flutter::SemanticsFlags::kIsEnabled)) {
817  traits |= UIAccessibilityTraitNotEnabled;
818  }
819  if (self.node.HasFlag(flutter::SemanticsFlags::kIsHeader)) {
820  traits |= UIAccessibilityTraitHeader;
821  }
822  if (self.node.HasFlag(flutter::SemanticsFlags::kIsImage)) {
823  traits |= UIAccessibilityTraitImage;
824  }
825  if (self.node.HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
826  traits |= UIAccessibilityTraitUpdatesFrequently;
827  }
828  if (self.node.HasFlag(flutter::SemanticsFlags::kIsLink)) {
829  traits |= UIAccessibilityTraitLink;
830  }
831  if (traits == UIAccessibilityTraitNone && ![self hasChildren] &&
832  self.accessibilityLabel.length != 0 &&
833  !self.node.HasFlag(flutter::SemanticsFlags::kIsTextField)) {
834  traits = UIAccessibilityTraitStaticText;
835  }
836  return traits;
837 }
838 
839 @end
840 
842 @property(nonatomic, weak) UIView* platformView;
843 @end
844 
846 
847 - (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
848  uid:(int32_t)uid
849  platformView:(nonnull FlutterTouchInterceptingView*)platformView {
850  if (self = [super initWithBridge:bridge uid:uid]) {
851  _platformView = platformView;
852  [platformView setFlutterAccessibilityContainer:self];
853  }
854  return self;
855 }
856 
857 - (id)nativeAccessibility {
858  return self.platformView;
859 }
860 
861 @end
862 
863 @implementation SemanticsObjectContainer {
864  fml::WeakPtr<flutter::AccessibilityBridgeIos> _bridge;
865 }
866 
867 #pragma mark - initializers
868 
869 - (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject
870  bridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge {
871  FML_DCHECK(semanticsObject) << "semanticsObject must be set";
872  // Initialize with the UIView as the container.
873  // The UIView will not necessarily be accessibility parent for this object.
874  // The bridge informs the OS of the actual structure via
875  // `accessibilityContainer` and `accessibilityElementAtIndex`.
876  self = [super initWithAccessibilityContainer:bridge->view()];
877 
878  if (self) {
879  _semanticsObject = semanticsObject;
880  _bridge = bridge;
881  }
882 
883  return self;
884 }
885 
886 #pragma mark - UIAccessibilityContainer overrides
887 
888 - (NSInteger)accessibilityElementCount {
889  return self.semanticsObject.children.count + 1;
890 }
891 
892 - (nullable id)accessibilityElementAtIndex:(NSInteger)index {
893  if (index < 0 || index >= [self accessibilityElementCount]) {
894  return nil;
895  }
896  if (index == 0) {
897  return self.semanticsObject.nativeAccessibility;
898  }
899 
900  SemanticsObject* child = self.semanticsObject.children[index - 1];
901 
902  if ([child hasChildren]) {
903  return child.accessibilityContainer;
904  }
905  return child.nativeAccessibility;
906 }
907 
908 - (NSInteger)indexOfAccessibilityElement:(id)element {
909  if (element == self.semanticsObject.nativeAccessibility) {
910  return 0;
911  }
912 
913  NSArray<SemanticsObject*>* children = self.semanticsObject.children;
914  for (size_t i = 0; i < [children count]; i++) {
915  SemanticsObject* child = children[i];
916  if ((![child hasChildren] && child.nativeAccessibility == element) ||
917  ([child hasChildren] && [child.nativeAccessibility accessibilityContainer] == element)) {
918  return i + 1;
919  }
920  }
921  return NSNotFound;
922 }
923 
924 #pragma mark - UIAccessibilityElement protocol
925 
926 - (BOOL)isAccessibilityElement {
927  return NO;
928 }
929 
930 - (CGRect)accessibilityFrame {
931  return self.semanticsObject.accessibilityFrame;
932 }
933 
934 - (id)accessibilityContainer {
935  if (!_bridge) {
936  return nil;
937  }
938  return ([self.semanticsObject uid] == kRootNodeId)
939  ? _bridge->view()
940  : self.semanticsObject.parent.accessibilityContainer;
941 }
942 
943 #pragma mark - UIAccessibilityAction overrides
944 
945 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
946  return [self.semanticsObject accessibilityScroll:direction];
947 }
948 
949 @end
self
return self
Definition: FlutterTextureRegistryRelay.mm:19
_inDealloc
BOOL _inDealloc
Definition: SemanticsObject.mm:258
SemanticsObject::parent
SemanticsObject * parent
Definition: SemanticsObject.h:42
FlutterCustomAccessibilityAction
Definition: SemanticsObject.h:135
FLUTTER_ASSERT_ARC::ApplyTransform
SkPoint ApplyTransform(SkPoint &point, const SkM44 &transform)
Definition: SemanticsObject.mm:47
FlutterSemanticsScrollView.h
-[SemanticsObject isAccessibilityBridgeAlive]
BOOL isAccessibilityBridgeAlive()
Definition: SemanticsObject.mm:329
FLUTTER_ASSERT_ARC::ConvertPointToGlobal
CGPoint ConvertPointToGlobal(SemanticsObject *reference, CGPoint local_point)
Definition: SemanticsObject.mm:52
FlutterCustomAccessibilityAction::uid
int32_t uid
Definition: SemanticsObject.h:140
FlutterSemanticsScrollView::isDoingSystemScrolling
BOOL isDoingSystemScrolling
Definition: FlutterSemanticsScrollView.h:43
FLUTTER_ASSERT_ARC::GetSemanticsActionForScrollDirection
flutter::SemanticsAction GetSemanticsActionForScrollDirection(UIAccessibilityScrollDirection direction)
Definition: SemanticsObject.mm:14
SemanticsObject::bridge
fml::WeakPtr< flutter::AccessibilityBridgeIos > bridge
Definition: SemanticsObject.h:52
FlutterScrollableSemanticsObject::scrollView
FlutterSemanticsScrollView * scrollView
Definition: SemanticsObject.h:190
FlutterSemanticsScrollView
Definition: FlutterSemanticsScrollView.h:21
FlutterSemanticsObject
Definition: SemanticsObject.h:155
FLUTTER_ASSERT_ARC::ConvertRectToGlobal
CGRect ConvertRectToGlobal(SemanticsObject *reference, CGRect local_rect)
Definition: SemanticsObject.mm:66
flutter
Definition: accessibility_bridge.h:27
kScrollExtentMaxForInf
constexpr float kScrollExtentMaxForInf
Definition: SemanticsObject.h:18
SemanticsObject::node
flutter::SemanticsNode node
Definition: SemanticsObject.h:57
FlutterPlatformViews_Internal.h
FlutterSwitchSemanticsObject
Definition: SemanticsObject.h:183
fml
Definition: profiler_metrics_ios.mm:41
kRootNodeId
constexpr int32_t kRootNodeId
Definition: SemanticsObject.h:16
SemanticsObject::hasChildren
BOOL hasChildren
Definition: SemanticsObject.h:62
SemanticsObject::children
NSArray< SemanticsObject * > * children
Definition: SemanticsObject.h:68
SemanticsObject::nativeAccessibility
id nativeAccessibility
Definition: SemanticsObject.h:83
SemanticsObject.h
FlutterPlatformViewSemanticsContainer
Definition: SemanticsObject.h:169
FlutterTouchInterceptingView
Definition: FlutterPlatformViews.mm:521
SemanticsObject::uid
int32_t uid
Definition: SemanticsObject.h:36
FLUTTER_ASSERT_ARC::GetGlobalTransform
SkM44 GetGlobalTransform(SemanticsObject *reference)
Definition: SemanticsObject.mm:39
-[SemanticsObject accessibilityBridgeDidFinishUpdate]
void accessibilityBridgeDidFinishUpdate()
Definition: SemanticsObject.mm:337
SemanticsObjectContainer
Definition: SemanticsObject.h:227
FLUTTER_ASSERT_ARC
Definition: FlutterChannelKeyResponder.mm:13
FlutterScrollableSemanticsObject
Definition: SemanticsObject.h:189
SemanticsObject
Definition: SemanticsObject.h:31