Skip to content

Commit

Permalink
Fixes accessibility issue that bridge fails to clean up FlutterScroll…
Browse files Browse the repository at this point in the history
…ableSemanticsObject (flutter#28495)
  • Loading branch information
chunhtai authored Sep 10, 2021
1 parent 57cd5ef commit 48ce284
Show file tree
Hide file tree
Showing 9 changed files with 587 additions and 255 deletions.
2 changes: 2 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,8 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginA
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPluginTest.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
Expand Down
2 changes: 2 additions & 0 deletions shell/platform/darwin/ios/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ source_set("flutter_framework_source") {
"framework/Source/FlutterPluginAppLifeCycleDelegate.mm",
"framework/Source/FlutterRestorationPlugin.h",
"framework/Source/FlutterRestorationPlugin.mm",
"framework/Source/FlutterSemanticsScrollView.h",
"framework/Source/FlutterSemanticsScrollView.mm",
"framework/Source/FlutterTextInputDelegate.h",
"framework/Source/FlutterTextInputPlugin.h",
"framework/Source/FlutterTextInputPlugin.mm",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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.

#ifndef SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTER_SEMANTICS_SCROLL_VIEW_H_
#define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTER_SEMANTICS_SCROLL_VIEW_H_

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN
@class SemanticsObject;

/**
* A UIScrollView to represent Flutter scrollable in iOS accessibility
* services.
*
* This class is hidden from the user and can't be interacted with. It
* sends all of selector calls from accessibility services to the
* owner SemanticsObject.
*/
@interface FlutterSemanticsScrollView : UIScrollView

- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder*)coder NS_UNAVAILABLE;
- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject;

@end
NS_ASSUME_NONNULL_END
#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTER_SEMANTICS_SCROLL_VIEW_H_
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// 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/FlutterSemanticsScrollView.h"

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

@interface FlutterSemanticsScrollView ()
@property(nonatomic, assign) SemanticsObject* semanticsObject;
@end

@implementation FlutterSemanticsScrollView

- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject {
self = [super initWithFrame:CGRectZero];
if (self) {
_semanticsObject = semanticsObject;
}
return self;
}

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
return nil;
}

// The following methods are explicitly forwarded to the wrapped SemanticsObject because the
// forwarding logic above doesn't apply to them since they are also implemented in the
// UIScrollView class, the base class.

- (BOOL)isAccessibilityElement {
if (![_semanticsObject isAccessibilityBridgeAlive]) {
return NO;
}

if ([_semanticsObject isAccessibilityElement]) {
return YES;
}
if (self.contentSize.width > self.frame.size.width ||
self.contentSize.height > self.frame.size.height) {
// In SwitchControl or VoiceControl, the isAccessibilityElement must return YES
// in order to use scroll actions.
return !_semanticsObject.bridge->isVoiceOverRunning();
} else {
return NO;
}
}

- (NSString*)accessibilityLabel {
return [_semanticsObject accessibilityLabel];
}

- (NSAttributedString*)accessibilityAttributedLabel {
return [_semanticsObject accessibilityAttributedLabel];
}

- (NSString*)accessibilityValue {
return [_semanticsObject accessibilityValue];
}

- (NSAttributedString*)accessibilityAttributedValue {
return [_semanticsObject accessibilityAttributedValue];
}

- (NSString*)accessibilityHint {
return [_semanticsObject accessibilityHint];
}

- (NSAttributedString*)accessibilityAttributedHint {
return [_semanticsObject accessibilityAttributedHint];
}

- (BOOL)accessibilityActivate {
return [_semanticsObject accessibilityActivate];
}

- (void)accessibilityIncrement {
[_semanticsObject accessibilityIncrement];
}

- (void)accessibilityDecrement {
[_semanticsObject accessibilityDecrement];
}

- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
return [_semanticsObject accessibilityScroll:direction];
}

- (BOOL)accessibilityPerformEscape {
return [_semanticsObject accessibilityPerformEscape];
}

- (void)accessibilityElementDidBecomeFocused {
[_semanticsObject accessibilityElementDidBecomeFocused];
}

- (void)accessibilityElementDidLoseFocus {
[_semanticsObject accessibilityElementDidLoseFocus];
}

- (id)accessibilityContainer {
return [_semanticsObject accessibilityContainer];
}

- (NSInteger)accessibilityElementCount {
return [[_semanticsObject children] count];
}

- (id)accessibilityElementAtIndex:(NSInteger)index {
SemanticsObject* child = [_semanticsObject children][index];

// Swap the original `SemanticsObject` to a `PlatformViewSemanticsContainer`
if (child.node.IsPlatformViewNode()) {
child.platformViewSemanticsContainer.index = index;
return child.platformViewSemanticsContainer;
}

if ([child hasChildren])
return [child accessibilityContainer];
return [child nativeAccessibility];
}

- (NSInteger)indexOfAccessibilityElement:(id)element {
if ([element isKindOfClass:[FlutterPlatformViewSemanticsContainer class]]) {
return ((FlutterPlatformViewSemanticsContainer*)element).index;
}

NSArray<SemanticsObject*>* children = [_semanticsObject children];
for (size_t i = 0; i < [children count]; i++) {
SemanticsObject* child = children[i];
if ((![child hasChildren] && child == element) ||
([child hasChildren] && [child accessibilityContainer] == element))
return i;
}
return NSNotFound;
}

@end
17 changes: 10 additions & 7 deletions shell/platform/darwin/ios/framework/Source/SemanticsObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ constexpr float kScrollExtentMaxForInf = 1000;
*/
@property(strong, nonatomic) FlutterPlatformViewSemanticsContainer* platformViewSemanticsContainer;

/**
* The UIAccessibility that represents this object.
*
* By default, this return self. Subclasses can override to return different
* objects to represent them. For example, FlutterScrollableSemanticsObject[s]
* maintain UIScrollView[s] to represent their UIAccessibility[s].
*/
@property(nonatomic, readonly) id nativeAccessibility;

/**
* Due to the fact that VoiceOver may hold onto SemanticObjects even after it shuts down,
* there can be situations where the AccessibilityBridge is shutdown, but the SemanticObject
Expand Down Expand Up @@ -171,13 +180,7 @@ constexpr float kScrollExtentMaxForInf = 1000;

/// The semantics object for scrollable. This class creates an UIScrollView to interact with the
/// iOS.
@interface FlutterScrollableSemanticsObject : UIScrollView

- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder*)coder NS_UNAVAILABLE;
- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject NS_DESIGNATED_INITIALIZER;
- (void)accessibilityBridgeDidFinishUpdate;
@interface FlutterScrollableSemanticsObject : SemanticsObject

@end

Expand Down
Loading

0 comments on commit 48ce284

Please sign in to comment.