| Flutter iOS Embedder |

SemanticsObject.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h"
6
7 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"
8 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h"
9
10 FLUTTER_ASSERT_ARC
11
12 namespace {
13
14 flutter::SemanticsAction GetSemanticsActionForScrollDirection(
15 UIAccessibilityScrollDirection direction) {
16 // To describe the vertical scroll direction, UIAccessibilityScrollDirection uses the
17 // direction the scroll bar moves in and SemanticsAction uses the direction the finger
18 // moves in. However, the horizontal scroll direction matches the SemanticsAction direction.
19 // That is way the following maps vertical opposite of the SemanticsAction, but the horizontal
20 // maps directly.
21 switch (direction) {
22 case UIAccessibilityScrollDirectionRight:
23 case UIAccessibilityScrollDirectionPrevious: // TODO(abarth): Support RTL using
24 // _node.textDirection.
25 return flutter::SemanticsAction::kScrollRight;
26 case UIAccessibilityScrollDirectionLeft:
27 case UIAccessibilityScrollDirectionNext: // TODO(abarth): Support RTL using
28 // _node.textDirection.
29 return flutter::SemanticsAction::kScrollLeft;
30 case UIAccessibilityScrollDirectionUp:
31 return flutter::SemanticsAction::kScrollDown;
32 case UIAccessibilityScrollDirectionDown:
33 return flutter::SemanticsAction::kScrollUp;
34 }
35 FML_DCHECK(false); // Unreachable
36 return flutter::SemanticsAction::kScrollUp;
37 }
38
39 SkM44 GetGlobalTransform(SemanticsObject* reference) {
40 SkM44 globalTransform = [reference node].transform;
41 for (SemanticsObject* parent = [reference parent]; parent; parent = parent.parent) {
42 globalTransform = parent.node.transform * globalTransform;
43 }
44 return globalTransform;
45 }
46
47 SkPoint ApplyTransform(SkPoint& point, const SkM44& transform) {
48 SkV4 vector = transform.map(point.x(), point.y(), 0, 1);
49 return SkPoint::Make(vector.x / vector.w, vector.y / vector.w);
50 }
51
52 CGPoint ConvertPointToGlobal(SemanticsObject* reference, CGPoint local_point) {
53 SkM44 globalTransform = GetGlobalTransform(reference);
54 SkPoint point = SkPoint::Make(local_point.x, local_point.y);
55 point = ApplyTransform(point, globalTransform);
56 // `rect` is in the physical pixel coordinate system. iOS expects the accessibility frame in
57 // the logical pixel coordinate system. Therefore, we divide by the `scale` (pixel ratio) to
58 // convert.
59 UIScreen* screen = reference.bridge->view().window.screen;
60 // Screen can be nil if the FlutterView is covered by another native view.
61 CGFloat scale = (screen ?: UIScreen.mainScreen).scale;
62 auto result = CGPointMake(point.x() / scale, point.y() / scale);
63 return [reference.bridge->view() convertPoint:result toView:nil];
64 }
65
66 CGRect ConvertRectToGlobal(SemanticsObject* reference, CGRect local_rect) {
67 SkM44 globalTransform = GetGlobalTransform(reference);
68
69 SkPoint quad[4] = {
70 SkPoint::Make(local_rect.origin.x, local_rect.origin.y), // top left
71 SkPoint::Make(local_rect.origin.x + local_rect.size.width, local_rect.origin.y), // top right
72 SkPoint::Make(local_rect.origin.x + local_rect.size.width,
73 local_rect.origin.y + local_rect.size.height), // bottom right
74 SkPoint::Make(local_rect.origin.x,
75 local_rect.origin.y + local_rect.size.height) // bottom left
76 };
77 for (auto& point : quad) {
78 point = ApplyTransform(point, globalTransform);
79 }
80 SkRect rect;
81 NSCAssert(rect.setBoundsCheck(quad, 4), @"Transformed points can't form a rect");
82 rect.setBounds(quad, 4);
83
84 // `rect` is in the physical pixel coordinate system. iOS expects the accessibility frame in
85 // the logical pixel coordinate system. Therefore, we divide by the `scale` (pixel ratio) to
86 // convert.
87 UIScreen* screen = reference.bridge->view().window.screen;
88 // Screen can be nil if the FlutterView is covered by another native view.
89 CGFloat scale = (screen ?: UIScreen.mainScreen).scale;
90 auto result =
91 CGRectMake(rect.x() / scale, rect.y() / scale, rect.width() / scale, rect.height() / scale);
92 return UIAccessibilityConvertFrameToScreenCoordinates(result, reference.bridge->view());
93 }
94
95 } // namespace
96
97 @interface FlutterSwitchSemanticsObject ()
98 @property(nonatomic, retain, readonly) UISwitch* nativeSwitch;
99 @end
100
101 @implementation FlutterSwitchSemanticsObject
102
103 - (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
104 uid:(int32_t)uid {
105 self = [super initWithBridge:bridge uid:uid];
106 if (self) {
107 _nativeSwitch = [[UISwitch alloc] init];
108 }
109 return self;
110 }
111
112 - (NSMethodSignature*)methodSignatureForSelector:(SEL)sel {
113 NSMethodSignature* result = [super methodSignatureForSelector:sel];
114 if (!result) {
115 result = [self.nativeSwitch methodSignatureForSelector:sel];
116 }
117 return result;
118 }
119
120 - (void)forwardInvocation:(NSInvocation*)anInvocation {
121 anInvocation.target = self.nativeSwitch;
122 [anInvocation invoke];
123 }
124
125 - (NSString*)accessibilityValue {
126 self.nativeSwitch.on = self.node.HasFlag(flutter::SemanticsFlags::kIsToggled) ||
127 self.node.HasFlag(flutter::SemanticsFlags::kIsChecked);
128
129 if (![self isAccessibilityBridgeAlive]) {
130 return nil;
131 } else {
132 return self.nativeSwitch.accessibilityValue;
133 }
134 }
135
136 - (UIAccessibilityTraits)accessibilityTraits {
137 self.nativeSwitch.enabled = self.node.HasFlag(flutter::SemanticsFlags::kIsEnabled);
138
139 return self.nativeSwitch.accessibilityTraits;
140 }
141
142 @end // FlutterSwitchSemanticsObject
143
144 @interface FlutterScrollableSemanticsObject ()
145 @property(nonatomic) FlutterSemanticsScrollView* scrollView;
146 @end
147
148 @implementation FlutterScrollableSemanticsObject
149
150 - (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
151 uid:(int32_t)uid {
152 self = [super initWithBridge:bridge uid:uid];
153 if (self) {
154 _scrollView = [[FlutterSemanticsScrollView alloc] initWithSemanticsObject:self];
155 [_scrollView setShowsHorizontalScrollIndicator:NO];
156 [_scrollView setShowsVerticalScrollIndicator:NO];
157 [self.bridge->view() addSubview:_scrollView];
158 }
159 return self;
160 }
161
162 - (void)dealloc {
163 [_scrollView removeFromSuperview];
164 }
165
166 - (void)accessibilityBridgeDidFinishUpdate {
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
829 @interface FlutterPlatformViewSemanticsContainer ()
830 @property(nonatomic, weak) UIView* platformView;
831 @end
832
833 @implementation FlutterPlatformViewSemanticsContainer
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
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
FlutterTouchInterceptingView
Definition: FlutterPlatformViews_Internal.h:138
FlutterScrollableSemanticsObject
Definition: SemanticsObject.h:188
SemanticsObject
Definition: SemanticsObject.h:30