Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit f1d3739

Browse files
authored
ios accessibility: started ignoring route changes when presenting modal view controllers (#18544)
1 parent 895ef33 commit f1d3739

File tree

5 files changed

+206
-8
lines changed

5 files changed

+206
-8
lines changed

shell/platform/darwin/ios/framework/Source/FlutterViewController.mm

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
@interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegate>
4040
@property(nonatomic, readwrite, getter=isDisplayingFlutterUI) BOOL displayingFlutterUI;
4141
@property(nonatomic, assign) BOOL isHomeIndicatorHidden;
42+
@property(nonatomic, assign) BOOL isPresentingViewControllerAnimating;
4243
@end
4344

4445
// The following conditional compilation defines an API 13 concept on earlier API targets so that
@@ -1240,4 +1241,22 @@ - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {
12401241
return [_engine.get() valuePublishedByPlugin:pluginKey];
12411242
}
12421243

1244+
- (void)presentViewController:(UIViewController*)viewControllerToPresent
1245+
animated:(BOOL)flag
1246+
completion:(void (^)(void))completion {
1247+
self.isPresentingViewControllerAnimating = YES;
1248+
[super presentViewController:viewControllerToPresent
1249+
animated:flag
1250+
completion:^{
1251+
self.isPresentingViewControllerAnimating = NO;
1252+
if (completion) {
1253+
completion();
1254+
}
1255+
}];
1256+
}
1257+
1258+
- (BOOL)isPresentingViewController {
1259+
return self.presentedViewController != nil || self.isPresentingViewControllerAnimating;
1260+
}
1261+
12431262
@end

shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ extern NSNotificationName const FlutterViewControllerShowHomeIndicator;
2323

2424
@interface FlutterViewController ()
2525

26+
@property(nonatomic, readonly) BOOL isPresentingViewController;
2627
- (fml::WeakPtr<FlutterViewController>)getWeakPtr;
2728
- (flutter::FlutterPlatformViewsController*)platformViewsController;
2829

shell/platform/darwin/ios/framework/Source/accessibility_bridge.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,21 @@ class PlatformViewIOS;
3636
*/
3737
class AccessibilityBridge final : public AccessibilityBridgeIos {
3838
public:
39+
/** Delegate for handling iOS operations. */
40+
class IosDelegate {
41+
public:
42+
virtual ~IosDelegate() = default;
43+
/// Returns true when the FlutterViewController associated with the `view`
44+
/// is presenting a modal view controller.
45+
virtual bool IsFlutterViewControllerPresentingModalViewController(UIView* view) = 0;
46+
virtual void PostAccessibilityNotification(UIAccessibilityNotifications notification,
47+
id argument) = 0;
48+
};
49+
3950
AccessibilityBridge(UIView* view,
4051
PlatformViewIOS* platform_view,
41-
FlutterPlatformViewsController* platform_views_controller);
52+
FlutterPlatformViewsController* platform_views_controller,
53+
std::unique_ptr<IosDelegate> ios_delegate = nullptr);
4254
~AccessibilityBridge();
4355

4456
void UpdateSemantics(flutter::SemanticsNodeUpdates nodes,
@@ -75,6 +87,7 @@ class AccessibilityBridge final : public AccessibilityBridgeIos {
7587
int32_t previous_route_id_;
7688
std::unordered_map<int32_t, flutter::CustomAccessibilityAction> actions_;
7789
std::vector<int32_t> previous_routes_;
90+
std::unique_ptr<IosDelegate> ios_delegate_;
7891

7992
FML_DISALLOW_COPY_AND_ASSIGN(AccessibilityBridge);
8093
};

shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#import "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h"
66
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h"
7+
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
78
#import "flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h"
89

910
#import "flutter/shell/platform/darwin/ios/platform_view_ios.h"
@@ -13,17 +14,54 @@
1314
FLUTTER_ASSERT_NOT_ARC
1415

1516
namespace flutter {
17+
namespace {
18+
19+
FlutterViewController* _Nullable GetFlutterViewControllerForView(UIView* view) {
20+
// There is no way to get a view's view controller in UIKit directly, this is
21+
// somewhat of a hacky solution to get that. This could be eliminated if the
22+
// bridge actually kept a reference to a FlutterViewController instead of a
23+
// UIView.
24+
id nextResponder = [view nextResponder];
25+
if ([nextResponder isKindOfClass:[FlutterViewController class]]) {
26+
return nextResponder;
27+
} else if ([nextResponder isKindOfClass:[UIView class]]) {
28+
return GetFlutterViewControllerForView(nextResponder);
29+
} else {
30+
return nil;
31+
}
32+
}
33+
34+
class DefaultIosDelegate : public AccessibilityBridge::IosDelegate {
35+
public:
36+
bool IsFlutterViewControllerPresentingModalViewController(UIView* view) override {
37+
FlutterViewController* viewController = GetFlutterViewControllerForView(view);
38+
if (viewController) {
39+
return viewController.isPresentingViewController;
40+
} else {
41+
return false;
42+
}
43+
}
44+
45+
void PostAccessibilityNotification(UIAccessibilityNotifications notification,
46+
id argument) override {
47+
UIAccessibilityPostNotification(notification, argument);
48+
}
49+
};
50+
} // namespace
1651

1752
AccessibilityBridge::AccessibilityBridge(UIView* view,
1853
PlatformViewIOS* platform_view,
19-
FlutterPlatformViewsController* platform_views_controller)
54+
FlutterPlatformViewsController* platform_views_controller,
55+
std::unique_ptr<IosDelegate> ios_delegate)
2056
: view_(view),
2157
platform_view_(platform_view),
2258
platform_views_controller_(platform_views_controller),
2359
objects_([[NSMutableDictionary alloc] init]),
2460
weak_factory_(this),
2561
previous_route_id_(0),
26-
previous_routes_({}) {
62+
previous_routes_({}),
63+
ios_delegate_(ios_delegate ? std::move(ios_delegate)
64+
: std::make_unique<DefaultIosDelegate>()) {
2765
accessibility_channel_.reset([[FlutterBasicMessageChannel alloc]
2866
initWithName:@"flutter/accessibility"
2967
binaryMessenger:platform_view->GetOwnerViewController().get().engine.binaryMessenger
@@ -137,15 +175,18 @@
137175

138176
layoutChanged = layoutChanged || [doomed_uids count] > 0;
139177
if (routeChanged) {
140-
NSString* routeName = [lastAdded routeName];
141-
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, routeName);
178+
if (!ios_delegate_->IsFlutterViewControllerPresentingModalViewController(view_)) {
179+
NSString* routeName = [lastAdded routeName];
180+
ios_delegate_->PostAccessibilityNotification(UIAccessibilityScreenChangedNotification,
181+
routeName);
182+
}
142183
} else if (layoutChanged) {
143184
// TODO(goderbauer): figure out which node to focus next.
144-
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
185+
ios_delegate_->PostAccessibilityNotification(UIAccessibilityLayoutChangedNotification, nil);
145186
}
146187
if (scrollOccured) {
147188
// TODO(tvolkert): provide meaningful string (e.g. "page 2 of 5")
148-
UIAccessibilityPostNotification(UIAccessibilityPageScrolledNotification, @"");
189+
ios_delegate_->PostAccessibilityNotification(UIAccessibilityPageScrolledNotification, @"");
149190
}
150191
}
151192

@@ -233,7 +274,7 @@ static bool DidFlagChange(const flutter::SemanticsNode& oldNode,
233274
NSString* type = annotatedEvent[@"type"];
234275
if ([type isEqualToString:@"announce"]) {
235276
NSString* message = annotatedEvent[@"data"][@"message"];
236-
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, message);
277+
ios_delegate_->PostAccessibilityNotification(UIAccessibilityAnnouncementNotification, message);
237278
}
238279
}
239280

shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,22 @@ void OnPlatformViewRegisterTexture(std::shared_ptr<Texture> texture) override {}
8686
void OnPlatformViewUnregisterTexture(int64_t texture_id) override {}
8787
void OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) override {}
8888
};
89+
90+
class MockIosDelegate : public AccessibilityBridge::IosDelegate {
91+
public:
92+
bool IsFlutterViewControllerPresentingModalViewController(UIView* view) override {
93+
return result_IsFlutterViewControllerPresentingModalViewController_;
94+
};
95+
96+
void PostAccessibilityNotification(UIAccessibilityNotifications notification,
97+
id argument) override {
98+
if (on_PostAccessibilityNotification_) {
99+
on_PostAccessibilityNotification_(notification, argument);
100+
}
101+
}
102+
std::function<void(UIAccessibilityNotifications, id)> on_PostAccessibilityNotification_;
103+
bool result_IsFlutterViewControllerPresentingModalViewController_ = false;
104+
};
89105
} // namespace
90106
} // namespace flutter
91107

@@ -238,4 +254,112 @@ - (void)testSemanticsDeallocated {
238254
XCTAssertNil(gMockPlatformView);
239255
}
240256

257+
- (void)testAnnouncesRouteChanges {
258+
flutter::MockDelegate mock_delegate;
259+
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
260+
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
261+
/*platform=*/thread_task_runner,
262+
/*raster=*/thread_task_runner,
263+
/*ui=*/thread_task_runner,
264+
/*io=*/thread_task_runner);
265+
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
266+
/*delegate=*/mock_delegate,
267+
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
268+
/*task_runners=*/runners);
269+
id mockFlutterView = OCMClassMock([FlutterView class]);
270+
std::string label = "some label";
271+
272+
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
273+
[[[NSMutableArray alloc] init] autorelease];
274+
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
275+
ios_delegate->on_PostAccessibilityNotification_ =
276+
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
277+
[accessibility_notifications addObject:@{
278+
@"notification" : @(notification),
279+
@"argument" : argument ? argument : [NSNull null],
280+
}];
281+
};
282+
__block auto bridge =
283+
std::make_unique<flutter::AccessibilityBridge>(/*view=*/mockFlutterView,
284+
/*platform_view=*/platform_view.get(),
285+
/*platform_views_controller=*/nil,
286+
/*ios_delegate=*/std::move(ios_delegate));
287+
288+
flutter::CustomAccessibilityActionUpdates actions;
289+
flutter::SemanticsNodeUpdates nodes;
290+
291+
flutter::SemanticsNode route_node;
292+
route_node.id = 1;
293+
route_node.label = label;
294+
route_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
295+
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
296+
route_node.label = "route";
297+
nodes[route_node.id] = route_node;
298+
flutter::SemanticsNode root_node;
299+
root_node.id = kRootNodeId;
300+
root_node.label = label;
301+
root_node.childrenInTraversalOrder = {1};
302+
root_node.childrenInHitTestOrder = {1};
303+
nodes[root_node.id] = root_node;
304+
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
305+
306+
XCTAssertEqual([accessibility_notifications count], 1ul);
307+
XCTAssertEqualObjects(accessibility_notifications[0][@"argument"], @"route");
308+
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
309+
UIAccessibilityScreenChangedNotification);
310+
}
311+
312+
- (void)testAnnouncesIgnoresRouteChangesWhenModal {
313+
flutter::MockDelegate mock_delegate;
314+
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
315+
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
316+
/*platform=*/thread_task_runner,
317+
/*raster=*/thread_task_runner,
318+
/*ui=*/thread_task_runner,
319+
/*io=*/thread_task_runner);
320+
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
321+
/*delegate=*/mock_delegate,
322+
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
323+
/*task_runners=*/runners);
324+
id mockFlutterView = OCMClassMock([FlutterView class]);
325+
std::string label = "some label";
326+
327+
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
328+
[[[NSMutableArray alloc] init] autorelease];
329+
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
330+
ios_delegate->on_PostAccessibilityNotification_ =
331+
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
332+
[accessibility_notifications addObject:@{
333+
@"notification" : @(notification),
334+
@"argument" : argument ? argument : [NSNull null],
335+
}];
336+
};
337+
ios_delegate->result_IsFlutterViewControllerPresentingModalViewController_ = true;
338+
__block auto bridge =
339+
std::make_unique<flutter::AccessibilityBridge>(/*view=*/mockFlutterView,
340+
/*platform_view=*/platform_view.get(),
341+
/*platform_views_controller=*/nil,
342+
/*ios_delegate=*/std::move(ios_delegate));
343+
344+
flutter::CustomAccessibilityActionUpdates actions;
345+
flutter::SemanticsNodeUpdates nodes;
346+
347+
flutter::SemanticsNode route_node;
348+
route_node.id = 1;
349+
route_node.label = label;
350+
route_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
351+
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
352+
route_node.label = "route";
353+
nodes[route_node.id] = route_node;
354+
flutter::SemanticsNode root_node;
355+
root_node.id = kRootNodeId;
356+
root_node.label = label;
357+
root_node.childrenInTraversalOrder = {1};
358+
root_node.childrenInHitTestOrder = {1};
359+
nodes[root_node.id] = root_node;
360+
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
361+
362+
XCTAssertEqual([accessibility_notifications count], 0ul);
363+
}
364+
241365
@end

0 commit comments

Comments
 (0)