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

UIAccessibility usability improvements for VoiceOver #535

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
86 changes: 81 additions & 5 deletions MBProgressHUD.m
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ - (void)commonInit {
self.alpha = 0.0f;
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.layer.allowsGroupOpacity = NO;
// Set this view's accessibility to false, as long as sub-elements are accessible
// Modal is used to prevent accessing elements behind "underneath" the progress HUD.
self.isAccessibilityElement = NO;
self.accessibilityViewIsModal = YES;

[self setupViews];
[self updateIndicators];
Expand Down Expand Up @@ -131,7 +135,7 @@ - (void)showAnimated:(BOOL)animated {
NSTimer *timer = [NSTimer timerWithTimeInterval:self.graceTime target:self selector:@selector(handleGraceTimer:) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self.graceTimer = timer;
}
}
// ... otherwise show the HUD immediately
else {
[self showUsingAnimation:self.useAnimation];
Expand All @@ -152,7 +156,7 @@ - (void)hideAnimated:(BOOL)animated {
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self.minShowTimer = timer;
return;
}
}
}
// ... otherwise hide the HUD immediately
[self hideUsingAnimation:self.useAnimation];
Expand Down Expand Up @@ -190,6 +194,28 @@ - (void)didMoveToSuperview {
[self updateForCurrentOrientationAnimated:NO];
}

#pragma mark - Accessibility

- (void)postAccessibilityScreenChangedNotificationWith:(id)element {
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, element);
}

- (void)postAccessibilityLayoutChangedNotificationWith:(id)element {
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, element);
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([object isKindOfClass:[UILabel class]] && self.label == (UILabel *)object) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(text))]) {
self.accessibilityLabel = self.label.text;
[self updateIndicators];
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}

#pragma mark - Internal show & hide operations

- (void)showUsingAnimation:(BOOL)animated {
Expand All @@ -206,11 +232,16 @@ - (void)showUsingAnimation:(BOOL)animated {
// Needed in case we hide and re-show with the same NSProgress object attached.
[self setNSProgressDisplayLinkEnabled:YES];

// Notify UIAccessibility that the HUD (self) is shown after animation completes.
if (animated) {
[self animateIn:YES withType:self.animationType completion:NULL];
[self animateIn:YES withType:self.animationType completion:^(BOOL finished) {
[self postAccessibilityScreenChangedNotificationWith:self];
}];
} else {
self.bezelView.alpha = 1.f;
self.backgroundView.alpha = 1.f;

[self postAccessibilityScreenChangedNotificationWith:self];
}
}

Expand All @@ -221,6 +252,7 @@ - (void)hideUsingAnimation:(BOOL)animated {
// call comes in while the HUD is animating out.
[self.hideDelayTimer invalidate];

// Note that we post UIAccessibility notifications in the -done method.
if (animated && self.showStarted) {
self.showStarted = nil;
[self animateIn:NO withType:self.animationType completion:^(BOOL finished) {
Expand Down Expand Up @@ -272,6 +304,10 @@ - (void)done {

if (self.hasFinished) {
self.alpha = 0.0f;

// Use a screen change on the superview to let UIAccessibility focus on the last clicked element.
[self postAccessibilityScreenChangedNotificationWith:self.superview];

if (self.removeFromSuperViewOnHide) {
[self removeFromSuperview];
}
Expand Down Expand Up @@ -314,6 +350,13 @@ - (void)setupViews {
label.font = [UIFont boldSystemFontOfSize:MBDefaultLabelFontSize];
label.opaque = NO;
label.backgroundColor = [UIColor clearColor];
// Disable accessibility element on the label, since with KVO, we use the text value on the indicator view's accessibilityLabel
// See updateIndicators to manually reset accessibilityElement for custom/text indicators
label.isAccessibilityElement = NO;
[label addObserver:self
forKeyPath:@"text"
options:NSKeyValueObservingOptionNew
context:nil];
_label = label;

UILabel *detailsLabel = [UILabel new];
Expand Down Expand Up @@ -375,16 +418,18 @@ - (void)updateIndicators {
}
else if (mode == MBProgressHUDModeDeterminate || mode == MBProgressHUDModeAnnularDeterminate) {
if (!isRoundIndicator) {
// Update to determinante indicator
// Update to determinate indicator
[indicator removeFromSuperview];
indicator = [[MBRoundProgressView alloc] init];
[self.bezelView addSubview:indicator];
}
if (mode == MBProgressHUDModeAnnularDeterminate) {
[(MBRoundProgressView *)indicator setAnnular:YES];
}
}
}
else if (mode == MBProgressHUDModeCustomView && self.customView != indicator) {
// For custom views, reenable label accessibility as the indicator is unknown
self.label.isAccessibilityElement = YES;
// Update custom view indicator
[indicator removeFromSuperview];
indicator = self.customView;
Expand All @@ -393,6 +438,10 @@ - (void)updateIndicators {
else if (mode == MBProgressHUDModeText) {
[indicator removeFromSuperview];
indicator = nil;

// For a text only HUD, make sure UIAccessibility focuses on the label (and that it is an accessibilityElement).
self.label.isAccessibilityElement = YES;
[self postAccessibilityLayoutChangedNotificationWith:self.label];
}
indicator.translatesAutoresizingMaskIntoConstraints = NO;
self.indicator = indicator;
Expand All @@ -404,6 +453,13 @@ - (void)updateIndicators {
[indicator setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisHorizontal];
[indicator setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisVertical];

// Since this method is called when label text is updated with KVO, ensure the indicator view uses the new label value for accessibility
indicator.accessibilityLabel = self.accessibilityLabel;

// If indicators are updated, notify UIAccessibility.
// This may seem redundant, but is needed if multiple mode changes are used.
[self postAccessibilityLayoutChangedNotificationWith:self];

[self updateViewsForColor:self.contentColor];
[self setNeedsUpdateConstraints];
}
Expand Down Expand Up @@ -686,6 +742,9 @@ - (void)setProgress:(float)progress {
UIView *indicator = self.indicator;
if ([indicator respondsToSelector:@selector(setProgress:)]) {
[(id)indicator setValue:@(self.progress) forKey:@"progress"];
// Setting accessibilityValue allows for a gradual and accurate description of progress.
// This is used in conjunction with the UpdatesFrequently Accessibility Trait.
indicator.accessibilityValue = [NSString stringWithFormat:@"%2.f %%", (progress * 100)];
}
}
}
Expand Down Expand Up @@ -808,6 +867,10 @@ - (id)initWithFrame:(CGRect)frame {
if (self) {
self.backgroundColor = [UIColor clearColor];
self.opaque = NO;
// Ensure that this is an accessibility element and set the trait to allow percentage completion to be accessible.
self.isAccessibilityElement = YES;
self.accessibilityLabel = NSLocalizedString(@"Progress", nil);
self.accessibilityTraits = UIAccessibilityTraitUpdatesFrequently;
_progress = 0.f;
_annular = NO;
_progressTintColor = [[UIColor alloc] initWithWhite:1.f alpha:1.f];
Expand All @@ -827,6 +890,7 @@ - (CGSize)intrinsicContentSize {
- (void)setProgress:(float)progress {
if (progress != _progress) {
_progress = progress;
self.accessibilityValue = [NSString stringWithFormat:@"%2.f %%", (progress * 100)];
[self setNeedsDisplay];
}
}
Expand All @@ -852,6 +916,8 @@ - (void)setBackgroundTintColor:(UIColor *)backgroundTintColor {
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();

self.accessibilityFrame = [self convertRect:rect toCoordinateSpace:[[UIScreen mainScreen] coordinateSpace]];

if (_annular) {
// Draw background
CGFloat lineWidth = 2.f;
Expand Down Expand Up @@ -919,6 +985,11 @@ - (id)initWithFrame:(CGRect)frame {
_progressRemainingColor = [UIColor clearColor];
self.backgroundColor = [UIColor clearColor];
self.opaque = NO;

// Ensure that this is an accessibility element and set the trait to allow percentage completion to be accessible.
self.isAccessibilityElement = YES;
self.accessibilityLabel = NSLocalizedString(@"Progress", nil);
self.accessibilityTraits = UIAccessibilityTraitUpdatesFrequently;
}
return self;
}
Expand All @@ -934,6 +1005,9 @@ - (CGSize)intrinsicContentSize {
- (void)setProgress:(float)progress {
if (progress != _progress) {
_progress = progress;
// Along with the UpdatesFrequently trait, this allows percentages to be read accessibly.
self.accessibilityValue = [NSString stringWithFormat:@"%2.f %%", (progress * 100)];

[self setNeedsDisplay];
}
}
Expand Down Expand Up @@ -963,6 +1037,8 @@ - (void)drawRect:(CGRect)rect {
CGContextSetStrokeColorWithColor(context,[_lineColor CGColor]);
CGContextSetFillColorWithColor(context, [_progressRemainingColor CGColor]);

self.accessibilityFrame = [self convertRect:rect toCoordinateSpace:[[UIScreen mainScreen] coordinateSpace]];

// Draw background and Border
CGFloat radius = (rect.size.height / 2) - 2;
CGContextMoveToPoint(context, 2, rect.size.height/2);
Expand Down