Skip to content

Commit

Permalink
[cp][ios] Link PlatformView back to semantics tree (#46471) (#46715)
Browse files Browse the repository at this point in the history
Cherry pick 250daad

Cherry pick issue flutter/flutter#136266

Description:

The PlatformView does not have a semantics container when added to semantics tree, this PR gives it a semantics container to ensure accessibility traversal works.

This fixes flutter/flutter#135504, which is a regression of b8332e3

Before b8332e3, the traversal works because the PlatformView is added to the accessibilityElements of the FlutterPlatFormViewSemanticsContainer, which implicitly made the FlutterPlatFormViewSemanticsContainer as the PlatformVIew's AccessibilityContainer. 

Now we use the PlatformVIew as the nativeAccessibility of the  FlutterPlatFormViewSemanticsContainer, we need to expilicitly set the container. 

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
  • Loading branch information
Chris Yang authored Oct 11, 2023
1 parent 7ccdde7 commit a8f7394
Show file tree
Hide file tree
Showing 12 changed files with 254 additions and 168 deletions.
6 changes: 6 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -2697,6 +2697,7 @@ ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextI
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextureRegistryRelay.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextureRegistryRelay.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextureRegistryRelayTest.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTouchInterceptingView_Test.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterUIPressProxy.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterUIPressProxy.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterUmbrellaImport.m + ../../../flutter/LICENSE
Expand All @@ -2718,6 +2719,8 @@ ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/KeyCodeMap_I
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTestMocks.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest_mrc.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/UIViewController_FlutterScreenAndSceneIfLoadedTest.mm + ../../../flutter/LICENSE
Expand Down Expand Up @@ -5465,6 +5468,7 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInp
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextureRegistryRelay.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextureRegistryRelay.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextureRegistryRelayTest.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTouchInterceptingView_Test.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterUIPressProxy.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterUIPressProxy.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterUmbrellaImport.m
Expand All @@ -5486,6 +5490,8 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/KeyCodeMap_Int
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTestMocks.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest_mrc.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/UIViewController_FlutterScreenAndSceneIfLoadedTest.mm
Expand Down
3 changes: 3 additions & 0 deletions shell/platform/darwin/ios/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,11 @@ source_set("ios_test_flutter_mrc") {
"framework/Source/FlutterEngineTest_mrc.mm",
"framework/Source/FlutterPlatformPluginTest.mm",
"framework/Source/FlutterPlatformViewsTest.mm",
"framework/Source/FlutterTouchInterceptingView_Test.h",
"framework/Source/FlutterViewControllerTest_mrc.mm",
"framework/Source/FlutterViewTest.mm",
"framework/Source/SemanticsObjectTestMocks.h",
"framework/Source/SemanticsObjectTest_mrc.mm",
"framework/Source/VsyncWaiterIosTest.mm",
"framework/Source/accessibility_bridge_test.mm",
"platform_message_handler_ios_test.mm",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,10 +406,15 @@ static bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect,
}

UIView* FlutterPlatformViewsController::GetPlatformViewByID(int64_t view_id) {
return [GetFlutterTouchInterceptingViewByID(view_id) embeddedView];
}

FlutterTouchInterceptingView* FlutterPlatformViewsController::GetFlutterTouchInterceptingViewByID(
int64_t view_id) {
if (views_.empty()) {
return nil;
}
return [touch_interceptors_[view_id].get() embeddedView];
return touch_interceptors_[view_id].get();
}

long FlutterPlatformViewsController::FindFirstResponderPlatformViewId() {
Expand Down Expand Up @@ -957,6 +962,10 @@ @implementation FlutterTouchInterceptingView {
fml::scoped_nsobject<DelayingGestureRecognizer> _delayingRecognizer;
FlutterPlatformViewGestureRecognizersBlockingPolicy _blockingPolicy;
UIView* _embeddedView;
// The used as the accessiblityContainer.
// The `accessiblityContainer` is used in UIKit to determine the parent of this accessibility
// node.
NSObject* _flutterAccessibilityContainer;
}
- (instancetype)initWithEmbeddedView:(UIView*)embeddedView
platformViewsController:
Expand Down Expand Up @@ -1035,6 +1044,14 @@ - (void)touchesCancelled:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
}

- (void)setFlutterAccessibilityContainer:(NSObject*)flutterAccessibilityContainer {
_flutterAccessibilityContainer = flutterAccessibilityContainer;
}

- (id)accessibilityContainer {
return _flutterAccessibilityContainer;
}

@end

@implementation DelayingGestureRecognizer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlatformViews.h"
#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTouchInterceptingView_Test.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
#import "flutter/shell/platform/darwin/ios/platform_view_ios.h"

Expand Down Expand Up @@ -3099,4 +3100,12 @@ - (void)testOnlyPlatformViewsAreRemovedWhenReset {
XCTAssertEqual(mockFlutterView.subviews.firstObject, someView);
}

- (void)testFlutterTouchInterceptingViewLinksToAccessibilityContainer {
FlutterTouchInterceptingView* touchInteceptorView =
[[[FlutterTouchInterceptingView alloc] init] autorelease];
NSObject* container = [[[NSObject alloc] init] autorelease];
[touchInteceptorView setFlutterAccessibilityContainer:container];
XCTAssertEqualObjects([touchInteceptorView accessibilityContainer], container);
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h"
#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlatformViews.h"
#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h"
#import "flutter/shell/platform/darwin/ios/ios_context.h"

@class FlutterTouchInterceptingView;
Expand Down Expand Up @@ -230,10 +231,17 @@ class FlutterPlatformViewsController {
// Returns the `FlutterPlatformView`'s `view` object associated with the view_id.
//
// If the `FlutterPlatformViewsController` does not contain any `FlutterPlatformView` object or
// a `FlutterPlatformView` object asscociated with the view_id cannot be found, the method
// a `FlutterPlatformView` object associated with the view_id cannot be found, the method
// returns nil.
UIView* GetPlatformViewByID(int64_t view_id);

// Returns the `FlutterTouchInterceptingView` with the view_id.
//
// If the `FlutterPlatformViewsController` does not contain any `FlutterPlatformView` object or
// a `FlutterPlatformView` object associated with the view_id cannot be found, the method
// returns nil.
FlutterTouchInterceptingView* GetFlutterTouchInterceptingViewByID(int64_t view_id);

PostPrerollResult PostPrerollAction(
const fml::RefPtr<fml::RasterThreadMerger>& raster_thread_merger);

Expand Down Expand Up @@ -424,6 +432,9 @@ class FlutterPlatformViewsController {

// Get embedded view
- (UIView*)embeddedView;

// Sets flutterAccessibilityContainer as this view's accessibilityContainer.
- (void)setFlutterAccessibilityContainer:(NSObject*)flutterAccessibilityContainer;
@end

@interface UIView (FirstResponder)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"

#ifndef SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTER_TOUCH_INTERCEPTING_VIEW_TEST_H_
#define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTER_TOUCH_INTERCEPTING_VIEW_TEST_H_

@interface FlutterTouchInterceptingView (Tests)
- (id)accessibilityContainer;
@end

#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTER_TOUCH_INTERCEPTING_VIEW_TESTS_H_
4 changes: 3 additions & 1 deletion shell/platform/darwin/ios/framework/Source/SemanticsObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ constexpr float kScrollExtentMaxForInf = 1000;

@class FlutterCustomAccessibilityAction;
@class FlutterPlatformViewSemanticsContainer;
@class FlutterTouchInterceptingView;

/**
* A node in the iOS semantics tree. This object is a wrapper over a native accessibiliy
Expand Down Expand Up @@ -171,7 +172,8 @@ constexpr float kScrollExtentMaxForInf = 1000;

- (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
uid:(int32_t)uid
platformView:(UIView*)platformView NS_DESIGNATED_INITIALIZER;
platformView:(FlutterTouchInterceptingView*)platformView
NS_DESIGNATED_INITIALIZER;

@end

Expand Down
13 changes: 3 additions & 10 deletions shell/platform/darwin/ios/framework/Source/SemanticsObject.mm
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,7 @@ @interface FlutterScrollableSemanticsObject ()
@property(nonatomic, retain) FlutterSemanticsScrollView* scrollView;
@end

@implementation FlutterScrollableSemanticsObject {
fml::scoped_nsobject<SemanticsObjectContainer> _container;
}
@implementation FlutterScrollableSemanticsObject

- (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
uid:(int32_t)uid {
Expand Down Expand Up @@ -865,9 +863,10 @@ @implementation FlutterPlatformViewSemanticsContainer

- (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
uid:(int32_t)uid
platformView:(nonnull UIView*)platformView {
platformView:(nonnull FlutterTouchInterceptingView*)platformView {
if (self = [super initWithBridge:bridge uid:uid]) {
_platformView = [platformView retain];
[platformView setFlutterAccessibilityContainer:self];
}
return self;
}
Expand All @@ -882,12 +881,6 @@ - (id)nativeAccessibility {
return _platformView;
}

#pragma mark - UIAccessibilityContainer overrides

- (NSArray*)accessibilityElements {
return @[ _platformView ];
}

@end

@implementation SemanticsObjectContainer {
Expand Down
155 changes: 1 addition & 154 deletions shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -7,97 +7,13 @@

#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTestMocks.h"

FLUTTER_ASSERT_ARC

const CGRect kScreenSize = CGRectMake(0, 0, 600, 800);

namespace flutter {
namespace {

class SemanticsActionObservation {
public:
SemanticsActionObservation(int32_t observed_id, SemanticsAction observed_action)
: id(observed_id), action(observed_action) {}

int32_t id;
SemanticsAction action;
};

class MockAccessibilityBridge : public AccessibilityBridgeIos {
public:
MockAccessibilityBridge() : observations({}) {
view_ = [[UIView alloc] initWithFrame:kScreenSize];
window_ = [[UIWindow alloc] initWithFrame:kScreenSize];
[window_ addSubview:view_];
}
bool isVoiceOverRunning() const override { return isVoiceOverRunningValue; }
UIView* view() const override { return view_; }
UIView<UITextInput>* textInputView() override { return nil; }
void DispatchSemanticsAction(int32_t id, SemanticsAction action) override {
SemanticsActionObservation observation(id, action);
observations.push_back(observation);
}
void DispatchSemanticsAction(int32_t id,
SemanticsAction action,
fml::MallocMapping args) override {
SemanticsActionObservation observation(id, action);
observations.push_back(observation);
}
void AccessibilityObjectDidBecomeFocused(int32_t id) override {}
void AccessibilityObjectDidLoseFocus(int32_t id) override {}
std::shared_ptr<FlutterPlatformViewsController> GetPlatformViewsController() const override {
return nil;
}
std::vector<SemanticsActionObservation> observations;
bool isVoiceOverRunningValue;

private:
UIView* view_;
UIWindow* window_;
};

class MockAccessibilityBridgeNoWindow : public AccessibilityBridgeIos {
public:
MockAccessibilityBridgeNoWindow() : observations({}) {
view_ = [[UIView alloc] initWithFrame:kScreenSize];
}
bool isVoiceOverRunning() const override { return isVoiceOverRunningValue; }
UIView* view() const override { return view_; }
UIView<UITextInput>* textInputView() override { return nil; }
void DispatchSemanticsAction(int32_t id, SemanticsAction action) override {
SemanticsActionObservation observation(id, action);
observations.push_back(observation);
}
void DispatchSemanticsAction(int32_t id,
SemanticsAction action,
fml::MallocMapping args) override {
SemanticsActionObservation observation(id, action);
observations.push_back(observation);
}
void AccessibilityObjectDidBecomeFocused(int32_t id) override {}
void AccessibilityObjectDidLoseFocus(int32_t id) override {}
std::shared_ptr<FlutterPlatformViewsController> GetPlatformViewsController() const override {
return nil;
}
std::vector<SemanticsActionObservation> observations;
bool isVoiceOverRunningValue;

private:
UIView* view_;
};
} // namespace
} // namespace flutter

@interface SemanticsObjectTest : XCTestCase
@end

@interface SemanticsObject (Tests)
- (BOOL)accessibilityScrollToVisible;
- (BOOL)accessibilityScrollToVisibleWithChild:(id)child;
- (id)_accessibilityHitTest:(CGPoint)point withEvent:(UIEvent*)event;
@end

@implementation SemanticsObjectTest

- (void)testCreate {
Expand Down Expand Up @@ -203,54 +119,6 @@ - (void)testAccessibilityHitTestNoFocusableItem {
XCTAssertNil(hitTestResult);
}

- (void)testAccessibilityHitTestSearchCanReturnPlatformView {
fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
new flutter::MockAccessibilityBridge());
fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
SemanticsObject* object0 = [[SemanticsObject alloc] initWithBridge:bridge uid:0];
SemanticsObject* object1 = [[SemanticsObject alloc] initWithBridge:bridge uid:1];
SemanticsObject* object3 = [[SemanticsObject alloc] initWithBridge:bridge uid:3];
UIView* platformView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
FlutterPlatformViewSemanticsContainer* platformViewSemanticsContainer =
[[FlutterPlatformViewSemanticsContainer alloc] initWithBridge:bridge
uid:1
platformView:platformView];

object0.children = @[ object1 ];
object0.childrenInHitTestOrder = @[ object1 ];
object1.children = @[ platformViewSemanticsContainer, object3 ];
object1.childrenInHitTestOrder = @[ platformViewSemanticsContainer, object3 ];

flutter::SemanticsNode node0;
node0.id = 0;
node0.rect = SkRect::MakeXYWH(0, 0, 200, 200);
node0.label = "0";
[object0 setSemanticsNode:&node0];

flutter::SemanticsNode node1;
node1.id = 1;
node1.rect = SkRect::MakeXYWH(0, 0, 200, 200);
node1.label = "1";
[object1 setSemanticsNode:&node1];

flutter::SemanticsNode node2;
node2.id = 2;
node2.rect = SkRect::MakeXYWH(0, 0, 100, 100);
node2.label = "2";
[platformViewSemanticsContainer setSemanticsNode:&node2];

flutter::SemanticsNode node3;
node3.id = 3;
node3.rect = SkRect::MakeXYWH(0, 0, 200, 200);
node3.label = "3";
[object3 setSemanticsNode:&node3];

CGPoint point = CGPointMake(10, 10);
id hitTestResult = [object0 _accessibilityHitTest:point withEvent:nil];

XCTAssertEqual(hitTestResult, platformView);
}

- (void)testAccessibilityScrollToVisible {
fml::WeakPtrFactory<flutter::MockAccessibilityBridge> factory(
new flutter::MockAccessibilityBridge());
Expand Down Expand Up @@ -897,27 +765,6 @@ - (void)testShouldDispatchShowOnScreenActionForHidden {
XCTAssertTrue(bridge->observations[0].action == flutter::SemanticsAction::kShowOnScreen);
}

- (void)testFlutterPlatformViewSemanticsContainer {
fml::WeakPtrFactory<flutter::MockAccessibilityBridge> factory(
new flutter::MockAccessibilityBridge());
fml::WeakPtr<flutter::MockAccessibilityBridge> bridge = factory.GetWeakPtr();
__weak UIView* weakPlatformView;
@autoreleasepool {
UIView* platformView = [[UIView alloc] init];

FlutterPlatformViewSemanticsContainer* container =
[[FlutterPlatformViewSemanticsContainer alloc] initWithBridge:bridge
uid:1
platformView:platformView];
XCTAssertEqualObjects(container.accessibilityElements, @[ platformView ]);
weakPlatformView = platformView;
XCTAssertNotNil(weakPlatformView);
}
// Check if there's no more strong references to `platformView` after container and platformView
// are released.
XCTAssertNil(weakPlatformView);
}

- (void)testFlutterSwitchSemanticsObjectMatchesUISwitch {
fml::WeakPtrFactory<flutter::MockAccessibilityBridge> factory(
new flutter::MockAccessibilityBridge());
Expand Down
Loading

0 comments on commit a8f7394

Please sign in to comment.