From d2a07ef741be244e4a0d72cf52cb0a66e3bff3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boris=20Du=C5=A1ek?= Date: Sun, 26 Apr 2015 00:13:00 +0200 Subject: [PATCH 1/4] Make utility buttons available through accessibility actions Assistive technologies like VoiceOver and Switch Control allow users to perform "actions" on a UI element. To enable that, we implemented an accessibility action for each utility button. The name of the action is the same as the accessibility label of the button. As UIAccessibilityCustomAction class needed for this is available only on iOS 8.0, care has been taken so that it works no matter with which SDK it was compiled and on what iOS version it is run. The underscored methods are underscored so as to make it clear they are not implementations of UIAccessibilityAction protocol methods, but rather internal supporting methods. --- SWTableViewCell.xcodeproj/project.pbxproj | 6 ++ .../PodFiles/SWAccessibilityCustomAction.h | 18 +++++ .../PodFiles/SWAccessibilityCustomAction.m | 15 ++++ SWTableViewCell/PodFiles/SWTableViewCell.m | 78 +++++++++++++++++++ 4 files changed, 117 insertions(+) create mode 100644 SWTableViewCell/PodFiles/SWAccessibilityCustomAction.h create mode 100644 SWTableViewCell/PodFiles/SWAccessibilityCustomAction.m diff --git a/SWTableViewCell.xcodeproj/project.pbxproj b/SWTableViewCell.xcodeproj/project.pbxproj index 50a61fb..e75abe4 100644 --- a/SWTableViewCell.xcodeproj/project.pbxproj +++ b/SWTableViewCell.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 6B23AE8B1AEC424000CACDB7 /* SWAccessibilityCustomAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B23AE8A1AEC424000CACDB7 /* SWAccessibilityCustomAction.m */; }; 810308911846579B00C378F0 /* NSMutableArray+SWUtilityButtons.m in Sources */ = {isa = PBXBuildFile; fileRef = 810308861846579B00C378F0 /* NSMutableArray+SWUtilityButtons.m */; }; 810308921846579B00C378F0 /* SWCellScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = 810308881846579B00C378F0 /* SWCellScrollView.m */; }; 810308931846579B00C378F0 /* SWLongPressGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 8103088A1846579B00C378F0 /* SWLongPressGestureRecognizer.m */; }; @@ -34,6 +35,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 6B23AE891AEC424000CACDB7 /* SWAccessibilityCustomAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SWAccessibilityCustomAction.h; sourceTree = ""; }; + 6B23AE8A1AEC424000CACDB7 /* SWAccessibilityCustomAction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SWAccessibilityCustomAction.m; sourceTree = ""; }; 810308851846579B00C378F0 /* NSMutableArray+SWUtilityButtons.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSMutableArray+SWUtilityButtons.h"; sourceTree = ""; }; 810308861846579B00C378F0 /* NSMutableArray+SWUtilityButtons.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSMutableArray+SWUtilityButtons.m"; sourceTree = ""; }; 810308871846579B00C378F0 /* SWCellScrollView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SWCellScrollView.h; sourceTree = ""; }; @@ -92,6 +95,8 @@ children = ( 810308851846579B00C378F0 /* NSMutableArray+SWUtilityButtons.h */, 810308861846579B00C378F0 /* NSMutableArray+SWUtilityButtons.m */, + 6B23AE891AEC424000CACDB7 /* SWAccessibilityCustomAction.h */, + 6B23AE8A1AEC424000CACDB7 /* SWAccessibilityCustomAction.m */, 810308871846579B00C378F0 /* SWCellScrollView.h */, 810308881846579B00C378F0 /* SWCellScrollView.m */, 810308891846579B00C378F0 /* SWLongPressGestureRecognizer.h */, @@ -249,6 +254,7 @@ files = ( AF34B77517DEE2B400BD9082 /* main.m in Sources */, AF34B77917DEE2B400BD9082 /* AppDelegate.m in Sources */, + 6B23AE8B1AEC424000CACDB7 /* SWAccessibilityCustomAction.m in Sources */, 810308921846579B00C378F0 /* SWCellScrollView.m in Sources */, 810308911846579B00C378F0 /* NSMutableArray+SWUtilityButtons.m in Sources */, 810308961846579B00C378F0 /* SWUtilityButtonView.m in Sources */, diff --git a/SWTableViewCell/PodFiles/SWAccessibilityCustomAction.h b/SWTableViewCell/PodFiles/SWAccessibilityCustomAction.h new file mode 100644 index 0000000..d8ebcc9 --- /dev/null +++ b/SWTableViewCell/PodFiles/SWAccessibilityCustomAction.h @@ -0,0 +1,18 @@ +// +// SWAccessibilityCustomAction.h +// SWTableViewCell +// +// Created by Boris Dušek on 4/25/15. +// Copyright (c) 2015 Chris Wendel. All rights reserved. +// + +#import + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 +@interface SWAccessibilityCustomAction : UIAccessibilityCustomAction + +@property(nonatomic, assign) BOOL right; +@property(nonatomic, assign) NSInteger index; + +@end +#endif diff --git a/SWTableViewCell/PodFiles/SWAccessibilityCustomAction.m b/SWTableViewCell/PodFiles/SWAccessibilityCustomAction.m new file mode 100644 index 0000000..9e78784 --- /dev/null +++ b/SWTableViewCell/PodFiles/SWAccessibilityCustomAction.m @@ -0,0 +1,15 @@ +// +// SWAccessibilityCustomAction.m +// SWTableViewCell +// +// Created by Boris Dušek (A11Y LTD.) on 4/25/15. +// Copyright (c) 2015 Chris Wendel. All rights reserved. +// + +#import "SWAccessibilityCustomAction.h" + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 +@implementation SWAccessibilityCustomAction + +@end +#endif diff --git a/SWTableViewCell/PodFiles/SWTableViewCell.m b/SWTableViewCell/PodFiles/SWTableViewCell.m index 3daceac..9db797e 100644 --- a/SWTableViewCell/PodFiles/SWTableViewCell.m +++ b/SWTableViewCell/PodFiles/SWTableViewCell.m @@ -8,6 +8,7 @@ #import "SWTableViewCell.h" #import "SWUtilityButtonView.h" +#import "SWAccessibilityCustomAction.h" static NSString * const kTableViewCellContentView = @"UITableViewCellContentView"; @@ -41,6 +42,12 @@ - (void)updateCellState; - (BOOL)shouldHighlight; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 +- (NSArray *)accessibilityCustomActions; +- (BOOL)_accessibilityActionActivated:(SWAccessibilityCustomAction *)action; +- (SWAccessibilityCustomAction *)_accessibilityActionForIndex:(NSInteger)index right:(BOOL)right; +#endif + @end @implementation SWTableViewCell { @@ -802,4 +809,75 @@ -(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveT return ![touch.view isKindOfClass:[UIControl class]]; } +#pragma mark - UIAccessibilityAction + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 + +- (NSArray*)accessibilityCustomActions +{ + if ([UIAccessibilityCustomAction class]) + { + NSMutableArray *actions = [NSMutableArray arrayWithCapacity:_leftUtilityButtons.count + _rightUtilityButtons.count]; + + // add actions from right area first as they are usually more important + for (NSInteger index = 0; index < _rightUtilityButtons.count; ++index) + { + [actions addObject:[self _accessibilityActionForIndex:index right:YES]]; + } + + for (NSInteger index = 0; index < _leftUtilityButtons.count; ++index) + { + [actions addObject:[self _accessibilityActionForIndex:index right:NO]]; + } + + NSArray *ret = [NSArray arrayWithArray:actions]; + return ret; + } + + return @[]; +} + +- (BOOL)_accessibilityActionActivated:(SWAccessibilityCustomAction*)action +{ + if (action.right) + { + if ([self.delegate respondsToSelector:@selector(swipeableTableViewCell:didTriggerRightUtilityButtonWithIndex:)]) + { + [self.delegate swipeableTableViewCell:self didTriggerRightUtilityButtonWithIndex:action.index]; + return true; + } + } + else + { + if ([self.delegate respondsToSelector:@selector(swipeableTableViewCell:didTriggerLeftUtilityButtonWithIndex:)]) + { + [self.delegate swipeableTableViewCell:self didTriggerLeftUtilityButtonWithIndex:action.index]; + return true; + } + } + + return false; +} + +- (SWAccessibilityCustomAction *)_accessibilityActionForIndex:(NSInteger)index right:(BOOL)right +{ + NSArray *buttons = right ? [self rightUtilityButtons] : [self leftUtilityButtons]; + UIButton *button = (UIButton *)[buttons objectAtIndex:index]; + + NSString *name = button.accessibilityLabel; + if (!name) + { + // UIKit throws exception when action name is nil and it was request by accessibility support; this cannot happen for icons loaded from file as they set accessibility label to the file name without suffix; so we would be setting it to the fallback name here only in the 0.01% cases where the icon is drawn in code using e.g. Core Graphics and has no inherent name + name = NSLocalizedString(@"Unnamed action", nil); + } + + SWAccessibilityCustomAction *action = [[SWAccessibilityCustomAction alloc] initWithName:name target:self selector:@selector(_accessibilityActionActivated:)]; + action.right = right; + action.index = index; + + return action; +} + +#endif + @end From 54abd687efb02733a8cbe9b28ecf948d710bc89f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boris=20Du=C5=A1ek?= Date: Sun, 26 Apr 2015 00:18:15 +0200 Subject: [PATCH 2/4] Convenience accessibility methods for array category This adds convenience accessibility methods that add accessibility labels to the utility buttons created with an image wihtout a title. This is important because buttons with titles automatically get their title as accessibility label, but buttons without a title and with an image nearly always need custom accessibility label, as the default is the image filename stripped of filename suffix. As a matter of fact, whenever one wants to add an image button using an image method of this category, they should always use the accessibility version (except when they already set the correct accessibility label on the UIImage, then it is not strictly necessary). So best would be to deprecate the "old" image methods that do not specify accessibility label. Still, when other accessibility properties (like hint or value) need to be customized (as well as non-accessibility properties), direct access to the button after adding it to the array can be used. --- .../PodFiles/NSMutableArray+SWUtilityButtons.h | 2 ++ .../PodFiles/NSMutableArray+SWUtilityButtons.m | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/SWTableViewCell/PodFiles/NSMutableArray+SWUtilityButtons.h b/SWTableViewCell/PodFiles/NSMutableArray+SWUtilityButtons.h index 87edf77..bb2a378 100644 --- a/SWTableViewCell/PodFiles/NSMutableArray+SWUtilityButtons.h +++ b/SWTableViewCell/PodFiles/NSMutableArray+SWUtilityButtons.h @@ -13,7 +13,9 @@ - (void)sw_addUtilityButtonWithColor:(UIColor *)color title:(NSString *)title; - (void)sw_addUtilityButtonWithColor:(UIColor *)color attributedTitle:(NSAttributedString *)title; - (void)sw_addUtilityButtonWithColor:(UIColor *)color icon:(UIImage *)icon; +- (void)sw_addUtilityButtonWithColor:(UIColor *)color icon:(UIImage *)icon accessibilityLabel:(NSString *)accessibilityLabel; - (void)sw_addUtilityButtonWithColor:(UIColor *)color normalIcon:(UIImage *)normalIcon selectedIcon:(UIImage *)selectedIcon; +- (void)sw_addUtilityButtonWithColor:(UIColor *)color normalIcon:(UIImage *)normalIcon selectedIcon:(UIImage *)selectedIcon accessibilityLabel:(NSString *)accessibilityLabel; @end diff --git a/SWTableViewCell/PodFiles/NSMutableArray+SWUtilityButtons.m b/SWTableViewCell/PodFiles/NSMutableArray+SWUtilityButtons.m index 2d32b75..70dbb9e 100644 --- a/SWTableViewCell/PodFiles/NSMutableArray+SWUtilityButtons.m +++ b/SWTableViewCell/PodFiles/NSMutableArray+SWUtilityButtons.m @@ -29,23 +29,35 @@ - (void)sw_addUtilityButtonWithColor:(UIColor *)color attributedTitle:(NSAttribu [self addObject:button]; } -- (void)sw_addUtilityButtonWithColor:(UIColor *)color icon:(UIImage *)icon +- (void)sw_addUtilityButtonWithColor:(UIColor *)color icon:(UIImage *)icon accessibilityLabel:(NSString *)accessibilityLabel { UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; button.backgroundColor = color; [button setImage:icon forState:UIControlStateNormal]; + button.accessibilityLabel = accessibilityLabel; [self addObject:button]; } -- (void)sw_addUtilityButtonWithColor:(UIColor *)color normalIcon:(UIImage *)normalIcon selectedIcon:(UIImage *)selectedIcon { +- (void)sw_addUtilityButtonWithColor:(UIColor *)color icon:(UIImage *)icon +{ + [self sw_addUtilityButtonWithColor:color icon:icon accessibilityLabel:nil]; +} + +- (void)sw_addUtilityButtonWithColor:(UIColor *)color normalIcon:(UIImage *)normalIcon selectedIcon:(UIImage *)selectedIcon accessibilityLabel:(NSString *)accessibilityLabel { UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; button.backgroundColor = color; [button setImage:normalIcon forState:UIControlStateNormal]; [button setImage:selectedIcon forState:UIControlStateHighlighted]; [button setImage:selectedIcon forState:UIControlStateSelected]; + button.accessibilityLabel = accessibilityLabel; [self addObject:button]; } +- (void)sw_addUtilityButtonWithColor:(UIColor *)color normalIcon:(UIImage *)normalIcon selectedIcon:(UIImage *)selectedIcon +{ + [self sw_addUtilityButtonWithColor:color normalIcon:normalIcon selectedIcon:selectedIcon accessibilityLabel:nil]; +} + @end From b6514c0cdd410e0da852ad3d46be80b9e9b3fd83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boris=20Du=C5=A1ek?= Date: Sun, 26 Apr 2015 01:22:10 +0200 Subject: [PATCH 3/4] Update README with accessible array methods Promote the accessibility methods from our array category in README.md. --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index acccd4a..f7cbee7 100644 --- a/README.md +++ b/README.md @@ -75,16 +75,19 @@ In your `tableView:cellForRowAtIndexPath:` method you set up the SWTableView cel [leftUtilityButtons sw_addUtilityButtonWithColor: [UIColor colorWithRed:0.07 green:0.75f blue:0.16f alpha:1.0] - icon:[UIImage imageNamed:@"check.png"]]; + icon:[UIImage imageNamed:@"check.png"] + accessibilityLabel:NSLocalizedString(@"Mark as done", nil)]; [leftUtilityButtons sw_addUtilityButtonWithColor: [UIColor colorWithRed:1.0f green:1.0f blue:0.35f alpha:1.0] - icon:[UIImage imageNamed:@"clock.png"]]; + icon:[UIImage imageNamed:@"clock.png"] + accessibilityLabel:NSLocalizedString(@"Schedule", nil)]; [leftUtilityButtons sw_addUtilityButtonWithColor: [UIColor colorWithRed:1.0f green:0.231f blue:0.188f alpha:1.0] icon:[UIImage imageNamed:@"cross.png"]]; [leftUtilityButtons sw_addUtilityButtonWithColor: [UIColor colorWithRed:0.55f green:0.27f blue:0.07f alpha:1.0] - icon:[UIImage imageNamed:@"list.png"]]; + icon:[UIImage imageNamed:@"list.png"] + accessibilityLabel:NSLocalizedString(@"Add to list", nil)]; return leftUtilityButtons; } From 1240be663b48336413a82e9fccf98b4b378ad13a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boris=20Du=C5=A1ek?= Date: Sun, 26 Apr 2015 00:27:24 +0200 Subject: [PATCH 4/4] Demonstrate accessibility options in the demo app Show different means of how accessibility actions for image-only utility buttons get their names: - by using the accessibility variant of the image methods on our NSMutableArray+SWUtilityButtons category - by adding accessibility label directly to the UIImage - by not adding any accessibility label at all (this is not desirable, it just shows the fallback accessibility label derived from the image file name) --- SWTableViewCell/ViewController.m | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/SWTableViewCell/ViewController.m b/SWTableViewCell/ViewController.m index 3efe130..4576ff9 100644 --- a/SWTableViewCell/ViewController.m +++ b/SWTableViewCell/ViewController.m @@ -172,16 +172,20 @@ - (NSArray *)leftButtons [leftUtilityButtons sw_addUtilityButtonWithColor: [UIColor colorWithRed:0.07 green:0.75f blue:0.16f alpha:1.0] - icon:[UIImage imageNamed:@"check.png"]]; + icon:[UIImage imageNamed:@"check.png"] accessibilityLabel:@"Mark as done"]; + + UIImage *clockIcon = [UIImage imageNamed:@"clock.png"]; + clockIcon.accessibilityLabel = @"Schedule"; [leftUtilityButtons sw_addUtilityButtonWithColor: [UIColor colorWithRed:1.0f green:1.0f blue:0.35f alpha:1.0] - icon:[UIImage imageNamed:@"clock.png"]]; + icon:clockIcon]; + [leftUtilityButtons sw_addUtilityButtonWithColor: [UIColor colorWithRed:1.0f green:0.231f blue:0.188f alpha:1.0] icon:[UIImage imageNamed:@"cross.png"]]; [leftUtilityButtons sw_addUtilityButtonWithColor: [UIColor colorWithRed:0.55f green:0.27f blue:0.07f alpha:1.0] - icon:[UIImage imageNamed:@"list.png"]]; + icon:[UIImage imageNamed:@"list.png"] accessibilityLabel:@"Add to list"]; return leftUtilityButtons; }