-
Notifications
You must be signed in to change notification settings - Fork 6k
ios accessibility: started ignoring route changes when presenting modal view controllers #18544
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
|
|
||
| #import "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h" | ||
| #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h" | ||
| #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" | ||
| #import "flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h" | ||
|
|
||
| #import "flutter/shell/platform/darwin/ios/platform_view_ios.h" | ||
|
|
@@ -13,17 +14,54 @@ | |
| FLUTTER_ASSERT_NOT_ARC | ||
|
|
||
| namespace flutter { | ||
| namespace { | ||
|
|
||
| FlutterViewController* _Nullable GetFlutterViewControllerForView(UIView* view) { | ||
| // There is no way to get a view's view controller in UIKit directly, this is | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like platformviewios right above it has a FlutterViewController anyway. We can just refactor this private constructor to take a FVC if needed.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea, i can look into it. i'll split it into a different PR to keep the bug fix and the refactor separate.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SG
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| // somewhat of a hacky solution to get that. This could be eliminated if the | ||
| // bridge actually kept a reference to a FlutterViewController instead of a | ||
| // UIView. | ||
| id nextResponder = [view nextResponder]; | ||
| if ([nextResponder isKindOfClass:[FlutterViewController class]]) { | ||
| return nextResponder; | ||
| } else if ([nextResponder isKindOfClass:[UIView class]]) { | ||
| return GetFlutterViewControllerForView(nextResponder); | ||
| } else { | ||
| return nil; | ||
| } | ||
| } | ||
|
|
||
| class DefaultIosDelegate : public AccessibilityBridge::IosDelegate { | ||
| public: | ||
| bool IsFlutterViewControllerPresentingModalViewController(UIView* view) override { | ||
| FlutterViewController* viewController = GetFlutterViewControllerForView(view); | ||
| if (viewController) { | ||
| return viewController.isPresentingViewController; | ||
| } else { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| void PostAccessibilityNotification(UIAccessibilityNotifications notification, | ||
| id argument) override { | ||
| UIAccessibilityPostNotification(notification, argument); | ||
| } | ||
| }; | ||
| } // namespace | ||
|
|
||
| AccessibilityBridge::AccessibilityBridge(UIView* view, | ||
| PlatformViewIOS* platform_view, | ||
| FlutterPlatformViewsController* platform_views_controller) | ||
| FlutterPlatformViewsController* platform_views_controller, | ||
| std::unique_ptr<IosDelegate> ios_delegate) | ||
| : view_(view), | ||
| platform_view_(platform_view), | ||
| platform_views_controller_(platform_views_controller), | ||
| objects_([[NSMutableDictionary alloc] init]), | ||
| weak_factory_(this), | ||
| previous_route_id_(0), | ||
| previous_routes_({}) { | ||
| previous_routes_({}), | ||
| ios_delegate_(ios_delegate ? std::move(ios_delegate) | ||
| : std::make_unique<DefaultIosDelegate>()) { | ||
| accessibility_channel_.reset([[FlutterBasicMessageChannel alloc] | ||
| initWithName:@"flutter/accessibility" | ||
| binaryMessenger:platform_view->GetOwnerViewController().get().engine.binaryMessenger | ||
|
|
@@ -137,15 +175,18 @@ | |
|
|
||
| layoutChanged = layoutChanged || [doomed_uids count] > 0; | ||
| if (routeChanged) { | ||
| NSString* routeName = [lastAdded routeName]; | ||
| UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, routeName); | ||
| if (!ios_delegate_->IsFlutterViewControllerPresentingModalViewController(view_)) { | ||
| NSString* routeName = [lastAdded routeName]; | ||
| ios_delegate_->PostAccessibilityNotification(UIAccessibilityScreenChangedNotification, | ||
| routeName); | ||
| } | ||
| } else if (layoutChanged) { | ||
| // TODO(goderbauer): figure out which node to focus next. | ||
| UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); | ||
| ios_delegate_->PostAccessibilityNotification(UIAccessibilityLayoutChangedNotification, nil); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why run the notification through the delegate as well?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For testing. We can't test that someone has called UIAccessibilityPostNotification so the delegate allows us to do dependency injection.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense. Thanks for the explanation :) |
||
| } | ||
| if (scrollOccured) { | ||
| // TODO(tvolkert): provide meaningful string (e.g. "page 2 of 5") | ||
| UIAccessibilityPostNotification(UIAccessibilityPageScrolledNotification, @""); | ||
| ios_delegate_->PostAccessibilityNotification(UIAccessibilityPageScrolledNotification, @""); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -233,7 +274,7 @@ static bool DidFlagChange(const flutter::SemanticsNode& oldNode, | |
| NSString* type = annotatedEvent[@"type"]; | ||
| if ([type isEqualToString:@"announce"]) { | ||
| NSString* message = annotatedEvent[@"data"][@"message"]; | ||
| UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, message); | ||
| ios_delegate_->PostAccessibilityNotification(UIAccessibilityAnnouncementNotification, message); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -86,6 +86,22 @@ void OnPlatformViewRegisterTexture(std::shared_ptr<Texture> texture) override {} | |
| void OnPlatformViewUnregisterTexture(int64_t texture_id) override {} | ||
| void OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) override {} | ||
| }; | ||
|
|
||
| class MockIosDelegate : public AccessibilityBridge::IosDelegate { | ||
| public: | ||
| bool IsFlutterViewControllerPresentingModalViewController(UIView* view) override { | ||
| return result_IsFlutterViewControllerPresentingModalViewController_; | ||
| }; | ||
|
|
||
| void PostAccessibilityNotification(UIAccessibilityNotifications notification, | ||
| id argument) override { | ||
| if (on_PostAccessibilityNotification_) { | ||
| on_PostAccessibilityNotification_(notification, argument); | ||
| } | ||
| } | ||
| std::function<void(UIAccessibilityNotifications, id)> on_PostAccessibilityNotification_; | ||
| bool result_IsFlutterViewControllerPresentingModalViewController_ = false; | ||
| }; | ||
| } // namespace | ||
| } // namespace flutter | ||
|
|
||
|
|
@@ -238,4 +254,112 @@ - (void)testSemanticsDeallocated { | |
| XCTAssertNil(gMockPlatformView); | ||
| } | ||
|
|
||
| - (void)testAnnouncesRouteChanges { | ||
| flutter::MockDelegate mock_delegate; | ||
| auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest"); | ||
| flutter::TaskRunners runners(/*label=*/self.name.UTF8String, | ||
| /*platform=*/thread_task_runner, | ||
| /*raster=*/thread_task_runner, | ||
| /*ui=*/thread_task_runner, | ||
| /*io=*/thread_task_runner); | ||
| auto platform_view = std::make_unique<flutter::PlatformViewIOS>( | ||
| /*delegate=*/mock_delegate, | ||
| /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, | ||
| /*task_runners=*/runners); | ||
| id mockFlutterView = OCMClassMock([FlutterView class]); | ||
| std::string label = "some label"; | ||
|
|
||
| NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications = | ||
| [[[NSMutableArray alloc] init] autorelease]; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should retain and release this instead of letting the autorelease pool handle it (usually
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In ObjC++ the rules are a bit different because of RAII. If I didn't put an autorelease here I should have a RAII wrapper around the pointer... I tried to get this file compiling with ARC but there is a hangup supporting that. Hopefully we can migrate it. |
||
| auto ios_delegate = std::make_unique<flutter::MockIosDelegate>(); | ||
| ios_delegate->on_PostAccessibilityNotification_ = | ||
| [accessibility_notifications](UIAccessibilityNotifications notification, id argument) { | ||
| [accessibility_notifications addObject:@{ | ||
| @"notification" : @(notification), | ||
| @"argument" : argument ? argument : [NSNull null], | ||
| }]; | ||
| }; | ||
| __block auto bridge = | ||
| std::make_unique<flutter::AccessibilityBridge>(/*view=*/mockFlutterView, | ||
| /*platform_view=*/platform_view.get(), | ||
| /*platform_views_controller=*/nil, | ||
| /*ios_delegate=*/std::move(ios_delegate)); | ||
|
|
||
| flutter::CustomAccessibilityActionUpdates actions; | ||
| flutter::SemanticsNodeUpdates nodes; | ||
|
|
||
| flutter::SemanticsNode route_node; | ||
| route_node.id = 1; | ||
| route_node.label = label; | ||
| route_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) | | ||
| static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute); | ||
| route_node.label = "route"; | ||
| nodes[route_node.id] = route_node; | ||
| flutter::SemanticsNode root_node; | ||
| root_node.id = kRootNodeId; | ||
| root_node.label = label; | ||
| root_node.childrenInTraversalOrder = {1}; | ||
| root_node.childrenInHitTestOrder = {1}; | ||
| nodes[root_node.id] = root_node; | ||
| bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions); | ||
|
|
||
| XCTAssertEqual([accessibility_notifications count], 1ul); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just removed it to see what happens and it still creates an error. |
||
| XCTAssertEqualObjects(accessibility_notifications[0][@"argument"], @"route"); | ||
| XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue], | ||
| UIAccessibilityScreenChangedNotification); | ||
| } | ||
|
|
||
| - (void)testAnnouncesIgnoresRouteChangesWhenModal { | ||
| flutter::MockDelegate mock_delegate; | ||
| auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest"); | ||
| flutter::TaskRunners runners(/*label=*/self.name.UTF8String, | ||
| /*platform=*/thread_task_runner, | ||
| /*raster=*/thread_task_runner, | ||
| /*ui=*/thread_task_runner, | ||
| /*io=*/thread_task_runner); | ||
| auto platform_view = std::make_unique<flutter::PlatformViewIOS>( | ||
| /*delegate=*/mock_delegate, | ||
| /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, | ||
| /*task_runners=*/runners); | ||
| id mockFlutterView = OCMClassMock([FlutterView class]); | ||
| std::string label = "some label"; | ||
|
|
||
| NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications = | ||
| [[[NSMutableArray alloc] init] autorelease]; | ||
| auto ios_delegate = std::make_unique<flutter::MockIosDelegate>(); | ||
| ios_delegate->on_PostAccessibilityNotification_ = | ||
| [accessibility_notifications](UIAccessibilityNotifications notification, id argument) { | ||
| [accessibility_notifications addObject:@{ | ||
| @"notification" : @(notification), | ||
| @"argument" : argument ? argument : [NSNull null], | ||
| }]; | ||
| }; | ||
| ios_delegate->result_IsFlutterViewControllerPresentingModalViewController_ = true; | ||
| __block auto bridge = | ||
| std::make_unique<flutter::AccessibilityBridge>(/*view=*/mockFlutterView, | ||
| /*platform_view=*/platform_view.get(), | ||
| /*platform_views_controller=*/nil, | ||
| /*ios_delegate=*/std::move(ios_delegate)); | ||
|
|
||
| flutter::CustomAccessibilityActionUpdates actions; | ||
| flutter::SemanticsNodeUpdates nodes; | ||
|
|
||
| flutter::SemanticsNode route_node; | ||
| route_node.id = 1; | ||
| route_node.label = label; | ||
| route_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) | | ||
| static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute); | ||
| route_node.label = "route"; | ||
| nodes[route_node.id] = route_node; | ||
| flutter::SemanticsNode root_node; | ||
| root_node.id = kRootNodeId; | ||
| root_node.label = label; | ||
| root_node.childrenInTraversalOrder = {1}; | ||
| root_node.childrenInHitTestOrder = {1}; | ||
| nodes[root_node.id] = root_node; | ||
| bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions); | ||
|
|
||
| XCTAssertEqual([accessibility_notifications count], 0ul); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ack |
||
| } | ||
|
|
||
| @end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is my own
UIViewControllerignorance showing, but what happens if you present two in a row? I'm guessing one animation finishes before the next one starts?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Presenting one while the other is animating? No idea. I would actually expect the first animation to be cancelled.