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