[_ASDisplayViewAccessiblity] Improve accessibility support (#2060)

* First approach to improve accessiblity

* Clear accessibleElements in addSubview: and willRemoveSubview:

* Adjust comments and rename viewNode to node

* Create new accessible elements if screen coordinates of view changes

* Remove legacy clearing of accessibleElements

* Performance improvements

* Use bounds for screenFrame calculation and indexOfObjectIdentical: in indexOfAccessiblityElement:

* Add ASDK_ACCESSIBILITY_DISABLE compiler flag to disable custom accessibility code in ASDK

* No need to set a frame if a subnode view is an accessibility element and not layer backed
This commit is contained in:
Michael Schneider 2016-08-16 10:27:14 -07:00 committed by Adlai Holler
parent 14ccb0b886
commit c65b2efed7
3 changed files with 117 additions and 47 deletions

View File

@ -16,6 +16,8 @@
@interface _ASDisplayView : UIView @interface _ASDisplayView : UIView
@property (copy, nonatomic) NSArray *accessibleElements;
// These methods expose a way for ASDisplayNode touch events to let the view call super touch events // These methods expose a way for ASDisplayNode touch events to let the view call super touch events
// Some UIKit mechanisms, like UITableView and UICollectionView selection handling, require this to work // Some UIKit mechanisms, like UITableView and UICollectionView selection handling, require this to work
- (void)__forwardTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; - (void)__forwardTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;

View File

@ -26,11 +26,15 @@
@implementation _ASDisplayView @implementation _ASDisplayView
{ {
__unsafe_unretained ASDisplayNode *_node; // Though UIView has a .node property added via category, since we can add an ivar to a subclass, use that for performance. __unsafe_unretained ASDisplayNode *_node; // Though UIView has a .node property added via category, since we can add an ivar to a subclass, use that for performance.
BOOL _inHitTest; BOOL _inHitTest;
BOOL _inPointInside; BOOL _inPointInside;
NSArray *_accessibleElements; NSArray *_accessibleElements;
CGRect _lastAccessibleElementsFrame;
} }
@synthesize accessibleElements = _accessibleElements;
@synthesize asyncdisplaykit_node = _node; @synthesize asyncdisplaykit_node = _node;
+ (Class)layerClass + (Class)layerClass
@ -126,7 +130,6 @@
[newSuperview.asyncdisplaykit_node addSubnode:_node]; [newSuperview.asyncdisplaykit_node addSubnode:_node];
} }
} }
} }
- (void)didMoveToSuperview - (void)didMoveToSuperview
@ -169,6 +172,24 @@
} }
} }
- (void)addSubview:(UIView *)view
{
[super addSubview:view];
#ifndef ASDK_ACCESSIBILITY_DISABLE
[self setAccessibleElements:nil];
#endif
}
- (void)willRemoveSubview:(UIView *)subview
{
[super willRemoveSubview:subview];
#ifndef ASDK_ACCESSIBILITY_DISABLE
[self setAccessibleElements:nil];
#endif
}
- (void)setNeedsDisplay - (void)setNeedsDisplay
{ {
// Standard implementation does not actually get to the layer, at least for views that don't implement drawRect:. // Standard implementation does not actually get to the layer, at least for views that don't implement drawRect:.

View File

@ -8,12 +8,39 @@
// of patent rights can be found in the PATENTS file in the same directory. // of patent rights can be found in the PATENTS file in the same directory.
// //
#ifndef ASDK_ACCESSIBILITY_DISABLE
#import "_ASDisplayView.h" #import "_ASDisplayView.h"
#import "ASDisplayNodeExtras.h" #import "ASDisplayNodeExtras.h"
#import "ASDisplayNode+FrameworkPrivate.h" #import "ASDisplayNode+FrameworkPrivate.h"
#pragma mark - UIAccessibilityElement #pragma mark - UIAccessibilityElement
typedef NSComparisonResult (^SortAccessibilityElementsComparator)(UIAccessibilityElement *, UIAccessibilityElement *);
/// Sort accessiblity elements first by y and than by x origin.
static 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(UIAccessibilityElement *a, UIAccessibilityElement *b) {
CGPoint originA = a.accessibilityFrame.origin;
CGPoint originB = b.accessibilityFrame.origin;
if (originA.y == originB.y) {
if (originA.x == originB.x) {
return NSOrderedSame;
}
return (originA.x < originB.x) ? NSOrderedAscending : NSOrderedDescending;
}
return (originA.y < originB.y) ? NSOrderedAscending : NSOrderedDescending;
};
});
[elements sortUsingComparator:comparator];
}
@implementation UIAccessibilityElement (_ASDisplayView) @implementation UIAccessibilityElement (_ASDisplayView)
+ (UIAccessibilityElement *)accessibilityElementWithContainer:(id)container node:(ASDisplayNode *)node + (UIAccessibilityElement *)accessibilityElementWithContainer:(id)container node:(ASDisplayNode *)node
@ -32,96 +59,116 @@
#pragma mark - _ASDisplayView / UIAccessibilityContainer #pragma mark - _ASDisplayView / UIAccessibilityContainer
static NSArray *ASCollectUIAccessibilityElementsForNode(ASDisplayNode *viewNode, ASDisplayNode *subnode, id container) { /// Collect all subnodes for the given node by walking down the subnode tree and calculates the screen coordinates based on the containerNode and container
NSMutableArray *accessibleElements = [NSMutableArray array]; static void CollectUIAccessibilityElementsForNode(ASDisplayNode *node, ASDisplayNode *containerNode, id container, NSMutableArray *elements)
ASDisplayNodePerformBlockOnEveryNodeBFS(subnode, ^(ASDisplayNode * _Nonnull currentNode) { {
ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray");
ASDisplayNodePerformBlockOnEveryNodeBFS(node, ^(ASDisplayNode * _Nonnull currentNode) {
// For every subnode that is layer backed or it's supernode has shouldRasterizeDescendants enabled // For every subnode that is layer backed or it's supernode has shouldRasterizeDescendants enabled
// we have to create a UIAccessibilityElement as no view for this node exists // we have to create a UIAccessibilityElement as no view for this node exists
if (currentNode != viewNode && currentNode.isAccessibilityElement) { if (currentNode != containerNode && currentNode.isAccessibilityElement) {
UIAccessibilityElement *accessibilityElement = [UIAccessibilityElement accessibilityElementWithContainer:container node:currentNode]; UIAccessibilityElement *accessibilityElement = [UIAccessibilityElement accessibilityElementWithContainer:container node:currentNode];
// As the node hierarchy is flattened it's necessary to convert the frame for each subnode in the tree to the // As the node hierarchy is flattened it's necessary to convert the frame for each subnode in the tree to the
// coordinate system of the supernode // coordinate system of the supernode
CGRect frame = [viewNode convertRect:currentNode.bounds fromNode:currentNode]; CGRect frame = [containerNode convertRect:currentNode.bounds fromNode:currentNode];
accessibilityElement.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(frame, container); accessibilityElement.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(frame, container);
[accessibleElements addObject:accessibilityElement]; [elements addObject:accessibilityElement];
} }
}); });
}
/// Collect all accessibliity elements for a given view and view node
static void CollectAccessibilityElementsForView(_ASDisplayView *view, NSMutableArray *elements)
{
ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray");
return [accessibleElements copy]; ASDisplayNode *node = view.asyncdisplaykit_node;
// Handle rasterize case
if (node.shouldRasterizeDescendants) {
CollectUIAccessibilityElementsForNode(node, node, view, elements);
return;
}
for (ASDisplayNode *subnode in node.subnodes) {
if (subnode.isAccessibilityElement) {
// An accessiblityElement can either be a UIView or a UIAccessibilityElement
if (subnode.isLayerBacked) {
// No view for layer backed nodes exist. It's necessary to create a UIAccessibilityElement that represents this node
UIAccessibilityElement *accessiblityElement = [UIAccessibilityElement accessibilityElementWithContainer:view node:subnode];
CGRect frame = [node convertRect:subnode.bounds fromNode:subnode];
[accessiblityElement setAccessibilityFrame:UIAccessibilityConvertFrameToScreenCoordinates(frame, view)];
[elements addObject:accessiblityElement];
} else {
// Accessiblity element is not layer backed just add the view as accessibility element
[elements addObject:subnode.view];
}
} else if (subnode.isLayerBacked) {
// Go down the hierarchy of the layer backed subnode and collect all of the UIAccessibilityElement
CollectUIAccessibilityElementsForNode(subnode, node, view, elements);
} else if ([subnode accessibilityElementCount] > 0) {
// UIView is itself a UIAccessibilityContainer just add it
[elements addObject:subnode.view];
}
}
} }
@interface _ASDisplayView () { @interface _ASDisplayView () {
NSArray *_accessibleElements; NSArray *_accessibleElements;
CGRect _lastAccessibleElementsFrame;
} }
@end @end
@implementation _ASDisplayView (UIAccessibilityContainer) @implementation _ASDisplayView (UIAccessibilityContainer)
#pragma mark - UIAccessibility #pragma mark - UIAccessibility
- (void)setAccessibleElements:(NSArray *)accessibleElements
{
_accessibleElements = nil;
}
- (NSArray *)accessibleElements - (NSArray *)accessibleElements
{ {
ASDisplayNode *viewNode = self.asyncdisplaykit_node; ASDisplayNode *viewNode = self.asyncdisplaykit_node;
if (viewNode == nil) { if (viewNode == nil) {
return nil; return @[];
} }
// Handle rasterize case CGRect screenFrame = UIAccessibilityConvertFrameToScreenCoordinates(self.bounds, self);
if (viewNode.shouldRasterizeDescendants) { if (_accessibleElements != nil && CGRectEqualToRect(_lastAccessibleElementsFrame, screenFrame)) {
_accessibleElements = ASCollectUIAccessibilityElementsForNode(viewNode, viewNode, self);
return _accessibleElements; return _accessibleElements;
} }
// Handle not rasterize case _lastAccessibleElementsFrame = screenFrame;
NSMutableArray *accessibleElements = [NSMutableArray array];
for (ASDisplayNode *subnode in viewNode.subnodes) { NSMutableArray *accessibleElements = [NSMutableArray array];
if (subnode.isAccessibilityElement) { CollectAccessibilityElementsForView(self, accessibleElements);
// An accessiblityElement can either be a UIView or a UIAccessibilityElement SortAccessibilityElements(accessibleElements);
id accessiblityElement = nil; _accessibleElements = accessibleElements;
if (subnode.isLayerBacked) {
// No view for layer backed nodes exist. It's necessary to create a UIAccessibilityElement that represents this node
accessiblityElement = [UIAccessibilityElement accessibilityElementWithContainer:self node:subnode];
} else {
accessiblityElement = subnode.view;
}
[accessiblityElement setAccessibilityFrame:UIAccessibilityConvertFrameToScreenCoordinates(subnode.frame, self)];
[accessibleElements addObject:accessiblityElement];
} else if (subnode.isLayerBacked) {
// Go down the hierarchy of the layer backed subnode and collect all of the UIAccessibilityElement
[accessibleElements addObjectsFromArray:ASCollectUIAccessibilityElementsForNode(viewNode, subnode, self)];
} else if ([subnode accessibilityElementCount] > 0) {
// Add UIAccessibilityContainer
[accessibleElements addObject:subnode.view];
}
}
_accessibleElements = [accessibleElements copy];
return _accessibleElements; return _accessibleElements;
} }
- (NSInteger)accessibilityElementCount - (NSInteger)accessibilityElementCount
{ {
return [self accessibleElements].count; return self.accessibleElements.count;
} }
- (id)accessibilityElementAtIndex:(NSInteger)index - (id)accessibilityElementAtIndex:(NSInteger)index
{ {
ASDisplayNodeAssertNotNil(_accessibleElements, @"At this point _accessibleElements should be created."); return self.accessibleElements[index];
if (_accessibleElements == nil) {
return nil;
}
return _accessibleElements[index];
} }
- (NSInteger)indexOfAccessibilityElement:(id)element - (NSInteger)indexOfAccessibilityElement:(id)element
{ {
if (_accessibleElements == nil) { return [self.accessibleElements indexOfObjectIdenticalTo:element];
return NSNotFound;
}
return [_accessibleElements indexOfObject:element];
} }
@end @end
#endif