Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -44373,6 +44373,7 @@ ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterViewT
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/IOKit.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/KeyCodeMap.g.mm + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/KeyCodeMap_Internal.h + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject+UIFocusSystem.mm + ../../../flutter/LICENSE
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
Expand Down Expand Up @@ -47261,6 +47262,7 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterViewTes
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/IOKit.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/KeyCodeMap.g.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/KeyCodeMap_Internal.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject+UIFocusSystem.mm
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
Expand Down
1 change: 1 addition & 0 deletions shell/platform/darwin/ios/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ source_set("flutter_framework_source_arc") {
"framework/Source/FlutterViewResponder.h",
"framework/Source/KeyCodeMap.g.mm",
"framework/Source/KeyCodeMap_Internal.h",
"framework/Source/SemanticsObject+UIFocusSystem.mm",
"framework/Source/SemanticsObject.h",
"framework/Source/SemanticsObject.mm",
"framework/Source/TextInputSemanticsObject.h",
Expand Down
27 changes: 27 additions & 0 deletions shell/platform/darwin/ios/framework/Source/FlutterView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

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

#include "flutter/fml/platform/darwin/cf_utils.h"

Expand Down Expand Up @@ -226,4 +227,30 @@ - (BOOL)isAccessibilityElement {
return NO;
}

// Enables keyboard-based navigation when the user turns on
// full keyboard access (FKA), using existing accessibility information.
//
// iOS does not provide any API for monitoring or querying whether FKA is on,
// but it does call isAccessibilityElement if FKA is on,
// so the isAccessibilityElement implementation above will be called
// when the view appears and the accessibility information will most likely
// be available by the time the user starts to interact with the app using FKA.
//
// See SemanticsObject+UIFocusSystem.mm for more details.
- (NSArray<id<UIFocusItem>>*)focusItemsInRect:(CGRect)rect {
NSObject* rootAccessibilityElement =
[self.accessibilityElements count] > 0 ? self.accessibilityElements[0] : nil;
return [rootAccessibilityElement isKindOfClass:[SemanticsObjectContainer class]]
? @[ [rootAccessibilityElement accessibilityElementAtIndex:0] ]
: nil;
}

- (NSArray<id<UIFocusEnvironment>>*)preferredFocusEnvironments {
// Occasionally we add subviews to FlutterView (text fields for example).
// These views shouldn't be directly visible to the iOS focus engine, instead
// the focus engine should only interact with the designated focus items
// (SemanticsObjects).
return nil;
}

@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// 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 "SemanticsObject.h"
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"

FLUTTER_ASSERT_ARC

// The SemanticsObject class conforms to UIFocusItem and UIFocusItemContainer
// protocols, so the SemanticsObject tree can also be used to represent
// interactive UI components on screen that can receive UIFocusSystem focus.
//
// Typically, physical key events received by the FlutterViewController is
// first delivered to the framework, but that stopped working for navigation keys
// since iOS 15 when full keyboard access (FKA) is on, because those events are
// consumed by the UIFocusSystem and never dispatched to the UIResponders in the
// application (see
// https://developer.apple.com/documentation/uikit/uikeycommand/3780513-wantspriorityoversystembehavior
// ). FKA relies on the iOS focus engine, to enable FKA on iOS 15+, we use
// SemanticsObject to provide the iOS focus engine with the required hierarchical
// information and geometric context.
//
// The focus engine focus is different from accessibility focus, or even the
// currentFocus of the Flutter FocusManager in the framework. On iOS 15+, FKA
// key events are dispatched to the current iOS focus engine focus (and
// translated to calls such as -[NSObject accessibilityActivate]), while most
// other key events are dispatched to the framework.
@interface SemanticsObject (UIFocusSystem) <UIFocusItem, UIFocusItemContainer>
@end

@implementation SemanticsObject (UIFocusSystem)

#pragma mark - UIFocusEnvironment Conformance

- (void)setNeedsFocusUpdate {
}

- (void)updateFocusIfNeeded {
}

- (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext*)context {
return YES;
}

- (void)didUpdateFocusInContext:(UIFocusUpdateContext*)context
withAnimationCoordinator:(UIFocusAnimationCoordinator*)coordinator {
}

- (id<UIFocusEnvironment>)parentFocusEnvironment {
// The root SemanticsObject node's parent is the FlutterView.
return self.parent ?: self.bridge->view();
}

- (NSArray<id<UIFocusEnvironment>>*)preferredFocusEnvironments {
return nil;
}

- (id<UIFocusItemContainer>)focusItemContainer {
return self;
}

#pragma mark - UIFocusItem Conformance

- (BOOL)canBecomeFocused {
if ((self.node.flags & static_cast<int32_t>(flutter::SemanticsFlags::kIsHidden)) != 0) {
return NO;
}
// Currently only supports SemanticsObjects that handle
// -[NSObject accessibilityActivate].
return self.node.HasAction(flutter::SemanticsAction::kTap);
}

- (CGRect)frame {
return self.accessibilityFrame;
}

#pragma mark - UIFocusItemContainer Conformance

- (NSArray<id<UIFocusItem>>*)focusItemsInRect:(CGRect)rect {
// It seems the iOS focus system rely heavily on this method (instead of
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: relies

nit: I know by "this method" you're referring to focusItemsInRect but because of the location of this comment, "this method" can be confused as "self.childrenInHitTestOrder"

// preferredFocusEnvironments) for directional navigation.
// Whether the item order in the returned array matters is unknown.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting 🫨 So if you set it to return a reversed "self.childrenInHitTestOrder", the traversing order is the same?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I guess the iOS focus engine re-organize the returned array using the coordinates reported by frame .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh never mind. I turned FKA off on the simulator that's why everything started working magically.

//
// Additionally, this method is only supposed to return items within the given
// rect but returning everything in the subtree seems to work fine.
return self.childrenInHitTestOrder;
}

- (id<UICoordinateSpace>)coordinateSpace {
return self.bridge->view();
}
@end
54 changes: 54 additions & 0 deletions shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

const float kFloatCompareEpsilon = 0.001;

@interface SemanticsObject (UIFocusSystem) <UIFocusItem, UIFocusItemContainer>
@end

@interface TextInputSemanticsObject (Test)
- (UIView<UITextInput>*)textInputSurrogate;
@end
Expand Down Expand Up @@ -1152,4 +1155,55 @@ - (void)testTextInputSemanticsObject_editActions {
[self waitForExpectationsWithTimeout:1 handler:nil];
}

- (void)testUIFocusItemConformance {
fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
new flutter::testing::MockAccessibilityBridge());
fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
SemanticsObject* parent = [[SemanticsObject alloc] initWithBridge:bridge uid:0];
SemanticsObject* child = [[SemanticsObject alloc] initWithBridge:bridge uid:1];
parent.children = @[ child ];

// parentFocusEnvironment
XCTAssertTrue([parent.parentFocusEnvironment isKindOfClass:[UIView class]]);
XCTAssertEqual(child.parentFocusEnvironment, child.parent);

// canBecomeFocused
flutter::SemanticsNode childNode;
childNode.flags = static_cast<int32_t>(flutter::SemanticsFlags::kIsHidden);
childNode.actions = static_cast<int32_t>(flutter::SemanticsAction::kTap);
[child setSemanticsNode:&childNode];
XCTAssertFalse(child.canBecomeFocused);
childNode.flags = 0;
[child setSemanticsNode:&childNode];
XCTAssertTrue(child.canBecomeFocused);
childNode.actions = 0;
[child setSemanticsNode:&childNode];
XCTAssertFalse(child.canBecomeFocused);

CGFloat scale = ((bridge->view().window.screen ?: UIScreen.mainScreen)).scale;

childNode.rect = SkRect::MakeXYWH(0, 0, 100 * scale, 100 * scale);
[child setSemanticsNode:&childNode];
flutter::SemanticsNode parentNode;
parentNode.rect = SkRect::MakeXYWH(0, 0, 200, 200);
[parent setSemanticsNode:&parentNode];

XCTAssertTrue(CGRectEqualToRect(child.frame, CGRectMake(0, 0, 100, 100)));
}

- (void)testUIFocusItemContainerConformance {
fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
new flutter::testing::MockAccessibilityBridge());
fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
SemanticsObject* parent = [[SemanticsObject alloc] initWithBridge:bridge uid:0];
SemanticsObject* child1 = [[SemanticsObject alloc] initWithBridge:bridge uid:1];
SemanticsObject* child2 = [[SemanticsObject alloc] initWithBridge:bridge uid:2];
parent.childrenInHitTestOrder = @[ child1, child2 ];

// focusItemsInRect
NSArray<id<UIFocusItem>>* itemsInRect = [parent focusItemsInRect:CGRectMake(0, 0, 100, 100)];
XCTAssertEqual(itemsInRect.count, (unsigned long)2);
XCTAssertTrue([itemsInRect containsObject:child1]);
XCTAssertTrue([itemsInRect containsObject:child2]);
}
@end