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