Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ASDisplayViewAccessibility] A few accessibility improvements #1812

Merged
merged 9 commits into from
May 6, 2020
Merged
Show file tree
Hide file tree
Changes from 7 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
28 changes: 3 additions & 25 deletions Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,16 @@ PODS:
- FBSnapshotTestCase/Core (2.1.4)
- JGMethodSwizzler (2.0.1)
- OCMock (3.4.1)
- PINCache (3.0.1-beta.7):
- PINCache/Arc-exception-safe (= 3.0.1-beta.7)
- PINCache/Core (= 3.0.1-beta.7)
- PINCache/Arc-exception-safe (3.0.1-beta.7):
- PINCache/Core
- PINCache/Core (3.0.1-beta.7):
- PINOperation (~> 1.1.1)
- PINOperation (1.1.1)
- PINRemoteImage (3.0.0-beta.14):
- PINRemoteImage/PINCache (= 3.0.0-beta.14)
- PINRemoteImage/Core (3.0.0-beta.14):
- PINOperation
- PINRemoteImage/PINCache (3.0.0-beta.14):
- PINCache (= 3.0.1-beta.7)
- PINRemoteImage/Core

DEPENDENCIES:
- FBSnapshotTestCase/Core (~> 2.1)
- JGMethodSwizzler (from `https://github.com/JonasGessner/JGMethodSwizzler`, branch `master`)
- OCMock (= 3.4.1)
- PINRemoteImage (= 3.0.0-beta.14)

SPEC REPOS:
https://github.com/cocoapods/specs.git:
https://github.com/CocoaPods/Specs.git:
- FBSnapshotTestCase
- OCMock
- PINCache
- PINOperation
- PINRemoteImage

EXTERNAL SOURCES:
JGMethodSwizzler:
Expand All @@ -46,10 +27,7 @@ SPEC CHECKSUMS:
FBSnapshotTestCase: 094f9f314decbabe373b87cc339bea235a63e07a
JGMethodSwizzler: 7328146117fffa8a4038c42eb7cd3d4c75006f97
OCMock: 2cd0716969bab32a2283ff3a46fd26a8c8b4c5e3
PINCache: 7cb9ae068c8f655717f7c644ef1dff9fd573e979
PINOperation: a6219e6fc9db9c269eb7a7b871ac193bcf400aac
PINRemoteImage: 81bbff853acc71c6de9e106e9e489a791b8bbb08

PODFILE CHECKSUM: 445046ac151568c694ff286684322273f0b597d6
PODFILE CHECKSUM: 345a6700f5fdec438ef5553e1eebf62653862733

COCOAPODS: 1.6.0
COCOAPODS: 1.9.1
1 change: 1 addition & 0 deletions Source/ASExperimentalFeatures.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ typedef NS_OPTIONS(NSUInteger, ASExperimentalFeatures) {
ASExperimentalDrawingGlobal = 1 << 10, // exp_drawing_global
ASExperimentalOptimizeDataControllerPipeline = 1 << 11, // exp_optimize_data_controller_pipeline
ASExperimentalTraitCollectionDidChangeWithPreviousCollection = 1 << 12, // exp_trait_collection_did_change_with_previous_collection
ASExperimentalDoNotCacheAccessibilityElements = 1 << 13, // exp_do_not_cache_accessibility_elements
ASExperimentalFeatureAll = 0xFFFFFFFF
};

Expand Down
3 changes: 2 additions & 1 deletion Source/ASExperimentalFeatures.mm
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
@"exp_oom_bg_dealloc_disable",
@"exp_drawing_global",
@"exp_optimize_data_controller_pipeline",
@"exp_trait_collection_did_change_with_previous_collection"]));
@"exp_trait_collection_did_change_with_previous_collection",
@"exp_do_not_cache_accessibility_elements"]));
if (flags == ASExperimentalFeatureAll) {
return allNames;
}
Expand Down
14 changes: 14 additions & 0 deletions Source/Details/_ASDisplayViewAccessiblity.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,17 @@
// should still work as long as accessibility is enabled, this framework provides no guarantees on
// their correctness. For details, see
// https://developer.apple.com/documentation/objectivec/nsobject/1615147-accessibilityelements

// After recusively collecting all of the accessibility elements of a node, they get sorted. This sort determines
// the order that a screen reader will traverse the elements. By default, we sort these elements based on their
// origin: lower y origin comes first, then lower x origin. If 2 nodes have an equal origin, the node with the smaller
// height is placed before the node with the smaller width. If two nodes have the exact same rect, we throw up our hands
// and return NSOrderedSame.
//
// In general this seems to work fairly well. However, if you want to provide a custom sort you can do so via
// setUserDefinedComparator(). The two elements you are comparing are NSObjects, which conforms to the informal
rcancro marked this conversation as resolved.
Show resolved Hide resolved
// UIAccessibility protocol, so you can safely compare properties like accessibilityFrame.
typedef NSComparisonResult (^ASSortAccessibilityElementsComparator)(NSObject *, NSObject *);

// Use this method to supply your own custom sort comparator used to determine the order of the accessibility elements
void setUserDefinedAccessibilitySortComparator(ASSortAccessibilityElementsComparator userDefinedComparator);
69 changes: 47 additions & 22 deletions Source/Details/_ASDisplayViewAccessiblity.mm
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#ifndef ASDK_ACCESSIBILITY_DISABLE

#import <AsyncDisplayKit/_ASDisplayView.h>
#import <AsyncDisplayKit/_ASDisplayViewAccessiblity.h>
#import <AsyncDisplayKit/ASAvailability.h>
#import <AsyncDisplayKit/ASCollectionNode.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
Expand All @@ -21,43 +22,55 @@

#pragma mark - UIAccessibilityElement

@protocol ASAccessibilityElementPositioning
static ASSortAccessibilityElementsComparator currentAccessibilityComparator = nil;
static ASSortAccessibilityElementsComparator defaultAccessibilityComparator = nil;

@property (nonatomic, readonly) CGRect accessibilityFrame;

@end

typedef NSComparisonResult (^SortAccessibilityElementsComparator)(id<ASAccessibilityElementPositioning>, id<ASAccessibilityElementPositioning>);
void setUserDefinedAccessibilitySortComparator(ASSortAccessibilityElementsComparator userDefinedComparator) {
currentAccessibilityComparator = userDefinedComparator ?: defaultAccessibilityComparator;
}

/// Sort accessiblity elements first by y and than by x origin.
static void SortAccessibilityElements(NSMutableArray *elements)
void SortAccessibilityElements(NSMutableArray *elements)
{
ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray");

static SortAccessibilityElementsComparator comparator = nil;

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
comparator = ^NSComparisonResult(id<ASAccessibilityElementPositioning> a, id<ASAccessibilityElementPositioning> b) {
CGPoint originA = a.accessibilityFrame.origin;
CGPoint originB = b.accessibilityFrame.origin;
if (originA.y == originB.y) {
if (originA.x == originB.x) {
return NSOrderedSame;
defaultAccessibilityComparator = ^NSComparisonResult(NSObject *a, NSObject *b) {
CGPoint originA = a.accessibilityFrame.origin;
CGPoint originB = b.accessibilityFrame.origin;
if (originA.y == originB.y) {
if (originA.x == originB.x) {
// if we have the same origin, favor shorter items. If heights are the same, favor thinner items. If size is the same ¯\_(ツ)_/¯
CGSize sizeA = a.accessibilityFrame.size;
CGSize sizeB = b.accessibilityFrame.size;
if (sizeA.height == sizeB.height) {
if (sizeA.width == sizeB.width) {
return NSOrderedSame;
}
return (sizeA.width < sizeB.width) ? NSOrderedAscending : NSOrderedDescending;
}
return (originA.x < originB.x) ? NSOrderedAscending : NSOrderedDescending;
return (sizeA.height < sizeB.height) ? NSOrderedAscending : NSOrderedDescending;
}
return (originA.y < originB.y) ? NSOrderedAscending : NSOrderedDescending;
};
return (originA.x < originB.x) ? NSOrderedAscending : NSOrderedDescending;
}
return (originA.y < originB.y) ? NSOrderedAscending : NSOrderedDescending;
};

if (!currentAccessibilityComparator) {
currentAccessibilityComparator = defaultAccessibilityComparator;
}
});
[elements sortUsingComparator:comparator];

[elements sortUsingComparator:currentAccessibilityComparator];
}

static CGRect ASAccessibilityFrameForNode(ASDisplayNode *node) {
CALayer *layer = node.layer;
return [layer convertRect:node.bounds toLayer:ASFindWindowOfLayer(layer).layer];
}

@interface ASAccessibilityElement : UIAccessibilityElement<ASAccessibilityElementPositioning>
@interface ASAccessibilityElement : UIAccessibilityElement

@property (nonatomic) ASDisplayNode *node;

Expand Down Expand Up @@ -93,7 +106,7 @@ - (CGRect)accessibilityFrame

#pragma mark - _ASDisplayView / UIAccessibilityContainer

@interface ASAccessibilityCustomAction : UIAccessibilityCustomAction<ASAccessibilityElementPositioning>
@interface ASAccessibilityCustomAction : UIAccessibilityCustomAction

@property (nonatomic) ASDisplayNode *node;

Expand Down Expand Up @@ -226,6 +239,16 @@ static void CollectAccessibilityElements(ASDisplayNode *node, NSMutableArray *el
}

for (ASDisplayNode *subnode in node.subnodes) {
// If a node is hidden or has an alpha of 0.0 we should not include it
if (subnode.hidden || subnode.alpha == 0.0) {
continue;
}
// If a subnode is outside of the view's window, exclude it
CGRect nodeInWindowCoords = [node convertRect:subnode.frame toNode:nil];
if (view.window && !CGRectIntersectsRect(view.window.frame, nodeInWindowCoords)) {
rcancro marked this conversation as resolved.
Show resolved Hide resolved
continue;
}

if (subnode.isAccessibilityElement) {
// An accessiblityElement can either be a UIView or a UIAccessibilityElement
if (subnode.isLayerBacked) {
Expand Down Expand Up @@ -275,7 +298,9 @@ - (NSArray *)accessibilityElements
return @[];
}

if (_accessibilityElements == nil) {
// when items become hidden/visible we have to manually clear the _accessibilityElements in order to get an updated version
// Instead, let's try computing the elements every time and see how badly it affects performance.
if (_accessibilityElements == nil || ASActivateExperimentalFeature(ASExperimentalDoNotCacheAccessibilityElements)) {
_accessibilityElements = [viewNode accessibilityElements];
}
return _accessibilityElements;
Expand Down
154 changes: 154 additions & 0 deletions Tests/ASDisplayViewAccessibilityTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@
#import <XCTest/XCTest.h>

#import <AsyncDisplayKit/_ASDisplayView.h>
#import <AsyncDisplayKit/_ASDisplayViewAccessiblity.h>
#import <AsyncDisplayKit/ASButtonNode.h>
#import <AsyncDisplayKit/ASDisplayNode.h>
#import <AsyncDisplayKit/ASDisplayNode+Beta.h>
#import <AsyncDisplayKit/ASTextNode.h>
#import <AsyncDisplayKit/ASConfiguration.h>
#import <AsyncDisplayKit/ASConfigurationInternal.h>
#import <AsyncDisplayKit/ASViewController.h>
#import <OCMock/OCMock.h>
#import "ASDisplayNodeTestsHelper.h"

extern void SortAccessibilityElements(NSMutableArray *elements);

@interface ASDisplayViewAccessibilityTests : XCTestCase
@end

Expand Down Expand Up @@ -251,4 +255,154 @@ - (void)testThatAccessibilityElementsOverrideWorks {
XCTAssertEqual(elements.firstObject, label);
}

- (void)testHiddenAccessibilityElements {
ASDisplayNode *containerNode = [[ASDisplayNode alloc] init];
containerNode.frame = CGRectMake(0, 0, 100, 200);

ASTextNode *label = [[ASTextNode alloc] init];
label.attributedText = [[NSAttributedString alloc] initWithString:@"test label"];
label.frame = CGRectMake(0, 0, 100, 20);

ASTextNode *hiddenLabel = [[ASTextNode alloc] init];
hiddenLabel.attributedText = [[NSAttributedString alloc] initWithString:@"hidden label"];
hiddenLabel.frame = CGRectMake(0, 24, 100, 20);
hiddenLabel.hidden = YES;

[containerNode addSubnode:label];
[containerNode addSubnode:hiddenLabel];

// force load
__unused UIView *view = containerNode.view;

NSArray *elements = [containerNode accessibilityElements];
XCTAssertTrue(elements.count == 1);
XCTAssertEqual(elements.firstObject, label.view);
}

- (void)testTransparentAccessibilityElements {
ASDisplayNode *containerNode = [[ASDisplayNode alloc] init];
containerNode.frame = CGRectMake(0, 0, 100, 200);

ASTextNode *label = [[ASTextNode alloc] init];
label.attributedText = [[NSAttributedString alloc] initWithString:@"test label"];
label.frame = CGRectMake(0, 0, 100, 20);

ASTextNode *hiddenLabel = [[ASTextNode alloc] init];
hiddenLabel.attributedText = [[NSAttributedString alloc] initWithString:@"hidden label"];
hiddenLabel.frame = CGRectMake(0, 24, 100, 20);
hiddenLabel.alpha = 0.0;

[containerNode addSubnode:label];
[containerNode addSubnode:hiddenLabel];

// force load
__unused UIView *view = containerNode.view;

NSArray *elements = [containerNode accessibilityElements];
XCTAssertTrue(elements.count == 1);
XCTAssertEqual(elements.firstObject, label.view);
}

- (void)testAccessibilityElementsNotInAppWindow {

UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 568)];
ASDisplayNode *node = [[ASDisplayNode alloc] init];
node.automaticallyManagesSubnodes = YES;

ASViewController *vc = [[ASViewController alloc] initWithNode:node];
window.rootViewController = vc;
[window makeKeyAndVisible];
[window layoutIfNeeded];

CGSize windowSize = window.frame.size;
ASTextNode *label = [[ASTextNode alloc] init];
label.attributedText = [[NSAttributedString alloc] initWithString:@"on screen"];
label.frame = CGRectMake(0, 0, 100, 20);

ASTextNode *partiallyOnScreenNodeY = [[ASTextNode alloc] init];
partiallyOnScreenNodeY.attributedText = [[NSAttributedString alloc] initWithString:@"partially on screen y"];
partiallyOnScreenNodeY.frame = CGRectMake(0, windowSize.height - 10, 100, 20);

ASTextNode *partiallyOnScreenNodeX = [[ASTextNode alloc] init];
partiallyOnScreenNodeX.attributedText = [[NSAttributedString alloc] initWithString:@"partially on screen x"];
partiallyOnScreenNodeX.frame = CGRectMake(windowSize.width - 10, 100, 100, 20);

ASTextNode *offScreenNodeY = [[ASTextNode alloc] init];
offScreenNodeY.attributedText = [[NSAttributedString alloc] initWithString:@"off screen y"];
offScreenNodeY.frame = CGRectMake(0, windowSize.height + 10, 100, 20);

ASTextNode *offScreenNodeX = [[ASTextNode alloc] init];
offScreenNodeX.attributedText = [[NSAttributedString alloc] initWithString:@"off screen x"];
offScreenNodeX.frame = CGRectMake(windowSize.width + 1, 200, 100, 20);

ASTextNode *offScreenNode = [[ASTextNode alloc] init];
offScreenNode.attributedText = [[NSAttributedString alloc] initWithString:@"off screen"];
offScreenNode.frame = CGRectMake(windowSize.width + 1, windowSize.height + 1, 100, 20);

[node addSubnode:label];
[node addSubnode:partiallyOnScreenNodeY];
[node addSubnode:partiallyOnScreenNodeX];
[node addSubnode:offScreenNodeY];
[node addSubnode:offScreenNodeX];
[node addSubnode:offScreenNode];

NSArray *elements = [node accessibilityElements];
XCTAssertTrue(elements.count == 3);
XCTAssertTrue([elements containsObject:label.view]);
XCTAssertTrue([elements containsObject:partiallyOnScreenNodeX.view]);
XCTAssertTrue([elements containsObject:partiallyOnScreenNodeY.view]);
}

- (void)testAccessibilitySort {
ASDisplayNode *node1 = [[ASDisplayNode alloc] init];
node1.accessibilityFrame = CGRectMake(0, 0, 50, 200);

ASDisplayNode *node2 = [[ASDisplayNode alloc] init];
node2.accessibilityFrame = CGRectMake(0, 0, 100, 200);

ASDisplayNode *node3 = [[ASDisplayNode alloc] init];
node3.accessibilityFrame = CGRectMake(0, 1, 100, 200);

ASDisplayNode *node4 = [[ASDisplayNode alloc] init];
node4.accessibilityFrame = CGRectMake(1, 1, 100, 200);

NSMutableArray *elements = [@[node2, node4, node3, node1] mutableCopy];
SortAccessibilityElements(elements);
XCTAssertEqual(elements[0], node1);
XCTAssertEqual(elements[1], node2);
XCTAssertEqual(elements[2], node3);
XCTAssertEqual(elements[3], node4);
}

- (void)testCustomAccessibilitySort {

// silly custom sorter that puts items with the largest height first.
setUserDefinedAccessibilitySortComparator(^NSComparisonResult(NSObject *a, NSObject *b) {
if (a.accessibilityFrame.size.height == b.accessibilityFrame.size.height) {
return NSOrderedSame;
}
return a.accessibilityFrame.size.height > b.accessibilityFrame.size.height ? NSOrderedAscending : NSOrderedDescending;
});

ASDisplayNode *node1 = [[ASDisplayNode alloc] init];
node1.accessibilityFrame = CGRectMake(0, 0, 50, 300);

ASDisplayNode *node2 = [[ASDisplayNode alloc] init];
node2.accessibilityFrame = CGRectMake(0, 0, 100, 250);

ASDisplayNode *node3 = [[ASDisplayNode alloc] init];
node3.accessibilityFrame = CGRectMake(0, 0, 100, 200);

ASDisplayNode *node4 = [[ASDisplayNode alloc] init];
node4.accessibilityFrame = CGRectMake(0, 0, 100, 150);

NSMutableArray *elements = [@[node2, node4, node3, node1] mutableCopy];
SortAccessibilityElements(elements);
XCTAssertEqual(elements[0], node1);
XCTAssertEqual(elements[1], node2);
XCTAssertEqual(elements[2], node3);
XCTAssertEqual(elements[3], node4);
}


@end