From 9350075812b524884003aa5b4e860ef856fcf49d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Sat, 9 May 2015 23:52:01 -0700 Subject: [PATCH 1/6] [ios] VoiceOver support for MGLMapView Enabled direct interaction with `MGLMapView`, allowing for panning and so forth. Defined an accessibility value that announces the current center coordinate. Inset the map view accessibility frame to exclude ornaments from the interactive element. --- platform/ios/src/MGLMapView.mm | 39 ++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 3d3666c6f65..34078430f8b 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -217,6 +217,8 @@ @implementation MGLMapView BOOL _delegateHasStrokeColorsForShapeAnnotations; BOOL _delegateHasFillColorsForShapeAnnotations; BOOL _delegateHasLineWidthsForShapeAnnotations; + + MGLCoordinateFormatter *_accessibilityCoordinateFormatter; } #pragma mark - Setup & Teardown - @@ -303,7 +305,14 @@ - (void)commonInit [self createGLView]; } + // setup accessibility + // + self.isAccessibilityElement = YES; self.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"MAP_A11Y_LABEL", nil, nil, @"Map", @"Accessibility label"); + self.accessibilityTraits = UIAccessibilityTraitAllowsDirectInteraction; + _accessibilityCoordinateFormatter = [[MGLCoordinateFormatter alloc] init]; + _accessibilityCoordinateFormatter.unitStyle = NSFormattingUnitStyleLong; + self.backgroundColor = [UIColor clearColor]; self.clipsToBounds = YES; @@ -1756,6 +1765,36 @@ - (void)emptyMemoryCache _mbglMap->onLowMemory(); } +#pragma mark - Accessibility - + +- (CGRect)accessibilityFrame +{ + CGRect frame = [super accessibilityFrame]; + UIViewController *viewController = self.viewControllerForLayoutGuides; + if (viewController) + { + UIView *compassContainer = self.compassView.superview; + CGFloat topInset = compassContainer.frame.origin.y + compassContainer.frame.size.height + 5; + frame.origin.y += topInset; + frame.size.height -= topInset; + + CGFloat bottomInset = MIN(self.logoView.frame.origin.y, self.attributionButton.frame.origin.y) - 8; + frame.size.height = bottomInset - frame.origin.y; + } + return frame; +} + +- (NSString *)accessibilityValue +{ + // Each arcminute of longitude is at most about 1 nmi, too small for low zoom levels. + // Each arcsecond of longitude is at most about 30 m, too small for all but the very highest of zoom levels. + _accessibilityCoordinateFormatter.allowsMinutes = self.zoomLevel > 8; + _accessibilityCoordinateFormatter.allowsSeconds = self.zoomLevel > 20; + + return [NSString stringWithFormat:@"Centered on %@", + [_accessibilityCoordinateFormatter stringFromCoordinate:self.centerCoordinate]]; +} + #pragma mark - Geography - + (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCenterCoordinate From 893a777a2f1c6188107952cc57b1d09e128a1bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Sun, 10 May 2015 01:23:00 -0700 Subject: [PATCH 2/6] [ios] Made compass and user dot accessible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made the compass behave like a button when visible. Added accessibility values and hints. The compass’s accessibility value uses the compass direction formatter. Turned off direct interaction with the map because it obscures the ornaments. The user dot now reports the current coordinates. Its accessibility label now reflects its annotation’s title instead of the hardcoded label “User location”. (It’d be weird to address the VoiceOver user as “user”.) --- .../resources/Base.lproj/Localizable.strings | 11 +++-- platform/ios/src/MGLMapView.mm | 42 +++++++++---------- .../ios/src/MGLUserLocationAnnotationView.m | 39 ++++++++++++++++- 3 files changed, 66 insertions(+), 26 deletions(-) diff --git a/platform/ios/resources/Base.lproj/Localizable.strings b/platform/ios/resources/Base.lproj/Localizable.strings index cf6de337c41..04cc667998c 100644 --- a/platform/ios/resources/Base.lproj/Localizable.strings +++ b/platform/ios/resources/Base.lproj/Localizable.strings @@ -7,6 +7,9 @@ /* No comment provided by engineer. */ "CANCEL" = "Cancel"; +/* Accessibility hint */ +"COMPASS_A11Y_HINT" = "Rotates the map to face due north"; + /* Accessibility label */ "COMPASS_A11Y_LABEL" = "Compass"; @@ -25,8 +28,11 @@ /* Setup documentation URL display string; keep as short as possible */ "FIRST_STEPS_URL" = "mapbox.com/help/first-steps-ios-sdk"; +/* Accessibility hint */ +"INFO_A11Y_HINT" = "Access credits, a feedback form, and more"; + /* Accessibility label */ -"INFO_A11Y_LABEL" = "Attribution info"; +"INFO_A11Y_LABEL" = "About this map"; /* Accessibility label */ "LOGO_A11Y_LABEL" = "Mapbox logo"; @@ -67,9 +73,6 @@ /* Telemetry prompt title */ "TELEMETRY_TITLE" = "Make Mapbox Maps Better"; -/* Accessibility label */ -"USER_DOT_A11Y_LABEL" = "User location"; - /* Default user location annotation title */ "USER_DOT_TITLE" = "You Are Here"; diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 34078430f8b..5b429d9d480 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -218,7 +218,7 @@ @implementation MGLMapView BOOL _delegateHasFillColorsForShapeAnnotations; BOOL _delegateHasLineWidthsForShapeAnnotations; - MGLCoordinateFormatter *_accessibilityCoordinateFormatter; + MGLCompassDirectionFormatter *_accessibilityCompassFormatter; } #pragma mark - Setup & Teardown - @@ -307,11 +307,11 @@ - (void)commonInit // setup accessibility // - self.isAccessibilityElement = YES; +// self.isAccessibilityElement = YES; self.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"MAP_A11Y_LABEL", nil, nil, @"Map", @"Accessibility label"); self.accessibilityTraits = UIAccessibilityTraitAllowsDirectInteraction; - _accessibilityCoordinateFormatter = [[MGLCoordinateFormatter alloc] init]; - _accessibilityCoordinateFormatter.unitStyle = NSFormattingUnitStyleLong; + _accessibilityCompassFormatter = [[MGLCompassDirectionFormatter alloc] init]; + _accessibilityCompassFormatter.unitStyle = NSFormattingUnitStyleLong; self.backgroundColor = [UIColor clearColor]; self.clipsToBounds = YES; @@ -365,7 +365,8 @@ - (void)commonInit // setup attribution // _attributionButton = [UIButton buttonWithType:UIButtonTypeInfoLight]; - _attributionButton.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"INFO_A11Y_LABEL", nil, nil, @"Attribution info", @"Accessibility label"); + _attributionButton.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"INFO_A11Y_LABEL", nil, nil, @"About this map", @"Accessibility label"); + _attributionButton.accessibilityHint = NSLocalizedStringWithDefaultValue(@"INFO_A11Y_HINT", nil, nil, @"Access credits, a feedback form, and more", @"Accessibility hint"); [_attributionButton addTarget:self action:@selector(showAttribution) forControlEvents:UIControlEventTouchUpInside]; _attributionButton.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:_attributionButton]; @@ -375,11 +376,14 @@ - (void)commonInit // setup compass // _compassView = [[UIImageView alloc] initWithImage:self.compassImage]; - _compassView.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"COMPASS_A11Y_LABEL", nil, nil, @"Compass", @"Accessibility label"); - _compassView.frame = CGRectMake(0, 0, _compassView.image.size.width, _compassView.image.size.height); + _compassView.frame = { CGPointZero, _compassView.image.size }; _compassView.alpha = 0; _compassView.userInteractionEnabled = YES; [_compassView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleCompassTapGesture:)]]; + _compassView.isAccessibilityElement = YES; + _compassView.accessibilityTraits = UIAccessibilityTraitButton; + _compassView.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"COMPASS_A11Y_LABEL", nil, nil, @"Compass", @"Accessibility label"); + _compassView.accessibilityHint = NSLocalizedStringWithDefaultValue(@"COMPASS_A11Y_HINT", nil, nil, @"Rotates the map to face due north", @"Accessibility hint"); UIView *container = [[UIView alloc] initWithFrame:CGRectZero]; [container addSubview:_compassView]; container.translatesAutoresizingMaskIntoConstraints = NO; @@ -663,6 +667,7 @@ - (void)updateConstraints multiplier:1 constant:5]]; + UIImage *compassImage = self.compassView.image; [compassContainerConstraints addObject: [NSLayoutConstraint constraintWithItem:compassContainer attribute:NSLayoutAttributeWidth @@ -670,7 +675,7 @@ - (void)updateConstraints toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 - constant:self.compassView.image.size.width]]; + constant:compassImage.size.width]]; [compassContainerConstraints addObject: [NSLayoutConstraint constraintWithItem:compassContainer @@ -679,7 +684,7 @@ - (void)updateConstraints toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 - constant:self.compassView.image.size.height]]; + constant:compassImage.size.height]]; [constraintParentView addConstraints:compassContainerConstraints]; // logo bug @@ -1784,17 +1789,6 @@ - (CGRect)accessibilityFrame return frame; } -- (NSString *)accessibilityValue -{ - // Each arcminute of longitude is at most about 1 nmi, too small for low zoom levels. - // Each arcsecond of longitude is at most about 30 m, too small for all but the very highest of zoom levels. - _accessibilityCoordinateFormatter.allowsMinutes = self.zoomLevel > 8; - _accessibilityCoordinateFormatter.allowsSeconds = self.zoomLevel > 20; - - return [NSString stringWithFormat:@"Centered on %@", - [_accessibilityCoordinateFormatter stringFromCoordinate:self.centerCoordinate]]; -} - #pragma mark - Geography - + (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCenterCoordinate @@ -3004,6 +2998,7 @@ - (void)selectAnnotation:(id )annotation animated:(BOOL)animated inView:self.glView constrainedToView:self.glView animated:animated]; + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); } // notify delegate @@ -3085,6 +3080,8 @@ - (void)deselectAnnotation:(id )annotation animated:(BOOL)animate self.calloutViewForSelectedAnnotation = nil; self.selectedAnnotation = nil; + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); + // notify delegate if ([self.delegate respondsToSelector:@selector(mapView:didDeselectAnnotation:)]) { @@ -3274,6 +3271,7 @@ - (void)setShowsUserLocation:(BOOL)showsUserLocation self.userLocationAnnotationView = [[MGLUserLocationAnnotationView alloc] initInMapView:self]; self.userLocationAnnotationView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin); + self.userLocationAnnotationView.isAccessibilityElement = YES; [self validateLocationServices]; } @@ -3855,6 +3853,7 @@ - (void)notifyMapChange:(mbgl::MapChange)change if ( ! [self isSuppressingChangeDelimiters] && [self.delegate respondsToSelector:@selector(mapView:regionDidChangeAnimated:)]) { + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); BOOL animated = change == mbgl::MapChangeRegionDidChangeAnimated; [self.delegate mapView:self regionDidChangeAnimated:animated]; } @@ -4034,8 +4033,9 @@ - (CGPoint)userLocationAnnotationViewCenter - (void)updateCompass { CLLocationDirection degrees = mbgl::util::wrap(-self.direction, 0., 360.); - self.compassView.transform = CGAffineTransformMakeRotation(MGLRadiansFromDegrees(degrees)); + + self.compassView.accessibilityValue = [_accessibilityCompassFormatter stringFromDirection:self.direction]; if (_mbglMap->getBearing() && self.compassView.alpha < 1) { diff --git a/platform/ios/src/MGLUserLocationAnnotationView.m b/platform/ios/src/MGLUserLocationAnnotationView.m index be93b788418..74908ec4e5e 100644 --- a/platform/ios/src/MGLUserLocationAnnotationView.m +++ b/platform/ios/src/MGLUserLocationAnnotationView.m @@ -4,6 +4,7 @@ #import "MGLUserLocation_Private.h" #import "MGLAnnotation.h" #import "MGLMapView.h" +#import "MGLCoordinateFormatter.h" #import "NSBundle+MGLAdditions.h" const CGFloat MGLUserLocationAnnotationDotSize = 22.0; @@ -37,6 +38,8 @@ @implementation MGLUserLocationAnnotationView CLLocationAccuracy _oldHorizontalAccuracy; double _oldZoom; double _oldPitch; + + MGLCoordinateFormatter *_accessibilityCoordinateFormatter; } - (instancetype)initWithFrame:(CGRect)frame @@ -54,7 +57,10 @@ - (instancetype)initInMapView:(MGLMapView *)mapView self.annotation = [[MGLUserLocation alloc] initWithMapView:mapView]; _mapView = mapView; [self setupLayers]; - self.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"USER_DOT_A11Y_LABEL", nil, nil, @"User location", @"Accessibility label"); + self.accessibilityTraits = UIAccessibilityTraitButton; + + _accessibilityCoordinateFormatter = [[MGLCoordinateFormatter alloc] init]; + _accessibilityCoordinateFormatter.unitStyle = NSFormattingUnitStyleLong; } return self; } @@ -65,6 +71,37 @@ - (instancetype)initWithCoder:(NSCoder *)decoder return [self initInMapView:mapView]; } +- (BOOL)isAccessibilityElement +{ + return !self.hidden; +} + +- (NSString *)accessibilityLabel +{ + return self.annotation.title; +} + +- (NSString *)accessibilityValue +{ + if (self.annotation.subtitle) + { + return self.annotation.subtitle; + } + + // Each arcminute of longitude is at most about 1 nmi, too small for low zoom levels. + // Each arcsecond of longitude is at most about 30 m, too small for all but the very highest of zoom levels. + double zoomLevel = self.mapView.zoomLevel; + _accessibilityCoordinateFormatter.allowsMinutes = zoomLevel > 8; + _accessibilityCoordinateFormatter.allowsSeconds = zoomLevel > 20; + + return [_accessibilityCoordinateFormatter stringFromCoordinate:self.mapView.centerCoordinate]; +} + +- (UIBezierPath *)accessibilityPath +{ + return [UIBezierPath bezierPathWithOvalInRect:self.frame]; +} + - (void)setTintColor:(UIColor *)tintColor { if (_puckModeActivated) From babc43ea6289a5ec6eee9dbf80057ab2de742dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Mon, 11 May 2015 03:16:37 -0700 Subject: [PATCH 3/6] [ios] Made annotations accessible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lazily create and cache accessibility elements for visible annotations as UIAccessibility asks about them. Sort the annotations’ accessibility elements by the annotations’ distance from the logical center (the center after accounting for padding, or the user dot when user tracking mode is on). Updated the changelog. Fixes #1493. --- platform/ios/CHANGELOG.md | 1 + platform/ios/src/MGLMapView.mm | 112 +++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md index 2377da7cf18..e36fd46290c 100644 --- a/platform/ios/CHANGELOG.md +++ b/platform/ios/CHANGELOG.md @@ -9,6 +9,7 @@ Mapbox welcomes participation and contributions from everyone. Please read [CON - The user dot now moves smoothly between user location updates while user location tracking is disabled. ([#1582](https://github.com/mapbox/mapbox-gl-native/pull/1582)) - An MGLAnnotation can be relocated by changing its `coordinate` property in a KVO-compliant way. An MGLMultiPoint cannot be relocated. ([#3835](https://github.com/mapbox/mapbox-gl-native/pull/3835)) - Setting the `image` property of an MGLAnnotationImage to `nil` resets it to the default red pin image and reclaims resources that can be used to customize additional annotations. ([#3835](https://github.com/mapbox/mapbox-gl-native/pull/3835)) +- The compass, user dot, and visible annotations are now accessible to VoiceOver users. ([#1496](https://github.com/mapbox/mapbox-gl-native/pull/1496)) - The SDK is now localizable. No localizations are currently provided, other than English, but if you need a particular localization, you can install the SDK manually and drop a .lproj folder into the framework. ([#4783](https://github.com/mapbox/mapbox-gl-native/pull/4783)) - Fixed an issue preventing KVO change notifications from being generated on MGLMapView’s `userTrackingMode` key path when `-setUserTrackingMode:animated:` is called. ([#4724](https://github.com/mapbox/mapbox-gl-native/pull/4724)) - Rendering now occurs on the main thread, fixing a hang when calling `-[MGLMapView styleURL]` before the map view has fully loaded or while the application is in the background. ([#2909](https://github.com/mapbox/mapbox-gl-native/pull/2909)) diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 5b429d9d480..222bd8f22b9 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -129,12 +129,23 @@ typedef NS_ENUM(NSUInteger, MGLUserTrackingState) { return {{ (float)r, (float)g, (float)b, (float)a }}; } +@interface MGLAnnotationAccessibilityElement : UIAccessibilityElement + +@property (nonatomic) MGLAnnotationTag tag; + +@end + +@implementation MGLAnnotationAccessibilityElement + +@end + /// Lightweight container for metadata about an annotation, including the annotation itself. class MGLAnnotationContext { public: id annotation; /// The annotation’s image’s reuse identifier. NSString *imageReuseIdentifier; + MGLAnnotationAccessibilityElement *accessibilityElement; }; #pragma mark - Private - @@ -1789,6 +1800,105 @@ - (CGRect)accessibilityFrame return frame; } +- (NSInteger)accessibilityElementCount +{ + std::vector visibleAnnotations = [self annotationTagsInRect:self.bounds]; + return visibleAnnotations.size() + 2 /* compass, attributionButton */; +} + +- (id)accessibilityElementAtIndex:(NSInteger)index +{ + std::vector visibleAnnotations = [self annotationTagsInRect:self.bounds]; + + // Ornaments + if (index == 0) + { + return self.compassView; + } + if (index > 0 && (NSUInteger)index == visibleAnnotations.size() + 1 /* compass */) + { + return self.attributionButton; + } + + std::sort(visibleAnnotations.begin(), visibleAnnotations.end()); + CGPoint centerPoint = self.contentCenter; + if (self.userTrackingMode != MGLUserTrackingModeNone) + { + centerPoint = self.userLocationAnnotationViewCenter; + } + CLLocationCoordinate2D currentCoordinate = [self convertPoint:centerPoint toCoordinateFromView:self]; + std::sort(visibleAnnotations.begin(), visibleAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) { + CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate]; + CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate]; + CLLocationDegrees deltaA = hypot(coordinateA.latitude - currentCoordinate.latitude, + coordinateA.longitude - currentCoordinate.longitude); + CLLocationDegrees deltaB = hypot(coordinateB.latitude - currentCoordinate.latitude, + coordinateB.longitude - currentCoordinate.longitude); + return deltaA < deltaB; + }); + + NSUInteger annotationIndex = MGLAnnotationTagNotFound; + if (index >= 0 && (NSUInteger)index < visibleAnnotations.size()) + { + annotationIndex = index - 1 /* compass */; + } + MGLAnnotationTag annotationTag = visibleAnnotations[annotationIndex]; + NSAssert(annotationTag != MGLAnnotationTagNotFound, @"Can’t get accessibility element for nonexistent or invisible annotation at index %li.", index); + NSAssert(_annotationContextsByAnnotationTag.count(annotationTag), @"Missing annotation for tag %u.", annotationTag); + MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag); + id annotation = annotationContext.annotation; + MGLAnnotationAccessibilityElement *element = annotationContext.accessibilityElement; + + // Lazily create an accessibility element for the found annotation. + if ( ! element) + { + element = [[MGLAnnotationAccessibilityElement alloc] initWithAccessibilityContainer:self]; + element.tag = annotationTag; + element.accessibilityTraits = UIAccessibilityTraitButton; + if ([annotation respondsToSelector:@selector(title)]) + { + element.accessibilityLabel = annotation.title; + } + if ([annotation respondsToSelector:@selector(subtitle)]) + { + element.accessibilityValue = annotation.subtitle; + } + annotationContext.accessibilityElement = element; + } + + // Update the accessibility element’s frame. + MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag]; + CGRect annotationFrame = [self frameOfImage:annotationImage.image centeredAtCoordinate:annotation.coordinate]; + CGRect screenRect = UIAccessibilityConvertFrameToScreenCoordinates(annotationFrame, self); + element.accessibilityFrame = screenRect; + + return element; +} + +- (NSInteger)indexOfAccessibilityElement:(id)element +{ + if (element == self.compassView) + { + return 0; + } + if ( ! [element isKindOfClass:[MGLAnnotationAccessibilityElement class]] && + element != self.attributionButton) + { + return NSNotFound; + } + + std::vector visibleAnnotations = [self annotationTagsInRect:self.bounds]; + if (element == self.attributionButton) + { + return visibleAnnotations.size(); + } + std::sort(visibleAnnotations.begin(), visibleAnnotations.end()); + auto foundElement = std::find(visibleAnnotations.begin(), visibleAnnotations.end(), + ((MGLAnnotationAccessibilityElement *)element).tag); + if (foundElement == visibleAnnotations.end()) return NSNotFound; + else return std::distance(visibleAnnotations.begin(), foundElement) + 1; +} + #pragma mark - Geography - + (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCenterCoordinate @@ -2551,6 +2661,7 @@ - (void)addAnnotations:(NS_ARRAY_OF(id ) *)annotations } [self didChangeValueForKey:@"annotations"]; + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); } /// Initialize and return a default annotation image that depicts a round pin @@ -2685,6 +2796,7 @@ - (void)removeAnnotations:(NS_ARRAY_OF(id ) *)annotations [self willChangeValueForKey:@"annotations"]; _mbglMap->removeAnnotations(annotationTagsToRemove); [self didChangeValueForKey:@"annotations"]; + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); } } From 9d3269eaa36d929de6191e60d7b332919ae02cab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Wed, 13 May 2015 11:47:06 -0700 Subject: [PATCH 4/6] [ios] Restored user dot accessibility Put the user dot back into rotation as an accessible element. --- platform/ios/src/MGLMapView.mm | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 222bd8f22b9..72f3bc915b9 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -1803,7 +1803,7 @@ - (CGRect)accessibilityFrame - (NSInteger)accessibilityElementCount { std::vector visibleAnnotations = [self annotationTagsInRect:self.bounds]; - return visibleAnnotations.size() + 2 /* compass, attributionButton */; + return visibleAnnotations.size() + 3 /* compass, userLocationAnnotationView, attributionButton */; } - (id)accessibilityElementAtIndex:(NSInteger)index @@ -1815,7 +1815,11 @@ - (id)accessibilityElementAtIndex:(NSInteger)index { return self.compassView; } - if (index > 0 && (NSUInteger)index == visibleAnnotations.size() + 1 /* compass */) + if (index == 1) + { + return self.userLocationAnnotationView; + } + if (index > 0 && (NSUInteger)index == visibleAnnotations.size() + 2 /* compass, userLocationAnnotationView */) { return self.attributionButton; } @@ -1838,9 +1842,9 @@ - (id)accessibilityElementAtIndex:(NSInteger)index }); NSUInteger annotationIndex = MGLAnnotationTagNotFound; - if (index >= 0 && (NSUInteger)index < visibleAnnotations.size()) + if (index >= 0 && (NSUInteger)(index - 2) < visibleAnnotations.size()) { - annotationIndex = index - 1 /* compass */; + annotationIndex = index - 2 /* compass, userLocationAnnotationView */; } MGLAnnotationTag annotationTag = visibleAnnotations[annotationIndex]; NSAssert(annotationTag != MGLAnnotationTagNotFound, @"Can’t get accessibility element for nonexistent or invisible annotation at index %li.", index); @@ -1881,6 +1885,10 @@ - (NSInteger)indexOfAccessibilityElement:(id)element { return 0; } + if (element == self.userLocationAnnotationView) + { + return 1; + } if ( ! [element isKindOfClass:[MGLAnnotationAccessibilityElement class]] && element != self.attributionButton) { @@ -1896,7 +1904,7 @@ - (NSInteger)indexOfAccessibilityElement:(id)element auto foundElement = std::find(visibleAnnotations.begin(), visibleAnnotations.end(), ((MGLAnnotationAccessibilityElement *)element).tag); if (foundElement == visibleAnnotations.end()) return NSNotFound; - else return std::distance(visibleAnnotations.begin(), foundElement) + 1; + else return std::distance(visibleAnnotations.begin(), foundElement) + 2 /* compass, userLocationAnnotationView */; } #pragma mark - Geography - @@ -4113,6 +4121,8 @@ - (void)updateUserLocationAnnotationViewAnimatedWithDuration:(NSTimeInterval)dur [self deselectAnnotation:self.selectedAnnotation animated:YES]; } } + + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); } /// Intended center point of the user location annotation view with respect to From 7cc3928a19ffc40e2835264f1349dc7a07fd4017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Sat, 16 May 2015 11:23:45 -0700 Subject: [PATCH 5/6] [ios] Made annotation callouts accessible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Via nfarina/calloutview#84, SMCalloutView is now accessible. Activating a focused annotation now shows its callout view and focuses its left accessory view, if present, or the title view. There is a “return to map” accessibility element for dismissing the callout view and restoring focus to the annotation on the map. --- platform/ios/app/MBXCustomCalloutView.m | 5 + platform/ios/app/MBXViewController.m | 18 +++ platform/ios/src/MGLCalloutView.h | 7 +- platform/ios/src/MGLMapView.mm | 133 ++++++++++++++---- .../ios/src/MGLUserLocationAnnotationView.m | 6 + 5 files changed, 142 insertions(+), 27 deletions(-) diff --git a/platform/ios/app/MBXCustomCalloutView.m b/platform/ios/app/MBXCustomCalloutView.m index 11ce86e76a5..9edc00f6e99 100644 --- a/platform/ios/app/MBXCustomCalloutView.m +++ b/platform/ios/app/MBXCustomCalloutView.m @@ -59,6 +59,11 @@ - (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToV CGFloat frameOriginY = rect.origin.y - frameHeight; self.frame = CGRectMake(frameOriginX, frameOriginY, frameWidth, frameHeight); + + if ([self.delegate respondsToSelector:@selector(calloutViewDidAppear:)]) + { + [self.delegate performSelector:@selector(calloutViewDidAppear:) withObject:self]; + } } - (void)dismissCalloutAnimated:(BOOL)animated diff --git a/platform/ios/app/MBXViewController.m b/platform/ios/app/MBXViewController.m index 8f628e8126c..098fc7b7441 100644 --- a/platform/ios/app/MBXViewController.m +++ b/platform/ios/app/MBXViewController.m @@ -739,6 +739,24 @@ - (void)mapView:(__unused MGLMapView *)mapView didChangeUserTrackingMode:(MGLUse return nil; } +- (UIView *)mapView:(__unused MGLMapView *)mapView leftCalloutAccessoryViewForAnnotation:(__unused id)annotation +{ + UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; + button.frame = CGRectZero; + [button setTitle:@"Left" forState:UIControlStateNormal]; + [button sizeToFit]; + return button; +} + +- (UIView *)mapView:(__unused MGLMapView *)mapView rightCalloutAccessoryViewForAnnotation:(__unused id)annotation +{ + UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; + button.frame = CGRectZero; + [button setTitle:@"Right" forState:UIControlStateNormal]; + [button sizeToFit]; + return button; +} + - (void)mapView:(MGLMapView *)mapView tapOnCalloutForAnnotation:(id )annotation { if ( ! [annotation isKindOfClass:[MGLPointAnnotation class]]) diff --git a/platform/ios/src/MGLCalloutView.h b/platform/ios/src/MGLCalloutView.h index 59f52adb6d9..641976dfeed 100644 --- a/platform/ios/src/MGLCalloutView.h +++ b/platform/ios/src/MGLCalloutView.h @@ -67,6 +67,11 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)calloutViewWillAppear:(UIView *)calloutView; +/** + Called after the callout view appears on screen, or after the appearance animation is complete. + */ +- (void)calloutViewDidAppear:(UIView *)calloutView; + @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 72f3bc915b9..f6a0cb4904e 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -148,6 +148,26 @@ @implementation MGLAnnotationAccessibilityElement MGLAnnotationAccessibilityElement *accessibilityElement; }; +/** An accessibility element representing the MGLMapView at large. */ +@interface MGLMapViewProxyAccessibilityElement : UIAccessibilityElement + +@end + +@implementation MGLMapViewProxyAccessibilityElement + +- (instancetype)initWithAccessibilityContainer:(id)container +{ + if (self = [super initWithAccessibilityContainer:container]) + { + self.accessibilityTraits = UIAccessibilityTraitButton; + self.accessibilityLabel = self.accessibilityLabel; + self.accessibilityHint = @"Returns to the map"; + } + return self; +} + +@end + #pragma mark - Private - @interface MGLMapView () *)calloutView +{ + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil); + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, calloutView); +} + - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { NSArray *validSimultaneousGestures = @[ self.pan, self.pinch, self.rotate ]; @@ -1789,25 +1832,53 @@ - (CGRect)accessibilityFrame UIViewController *viewController = self.viewControllerForLayoutGuides; if (viewController) { - UIView *compassContainer = self.compassView.superview; - CGFloat topInset = compassContainer.frame.origin.y + compassContainer.frame.size.height + 5; + CGFloat topInset = viewController.topLayoutGuide.length; frame.origin.y += topInset; - frame.size.height -= topInset; - - CGFloat bottomInset = MIN(self.logoView.frame.origin.y, self.attributionButton.frame.origin.y) - 8; - frame.size.height = bottomInset - frame.origin.y; + frame.size.height -= topInset + viewController.bottomLayoutGuide.length; } return frame; } +- (UIBezierPath *)accessibilityPath +{ + UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.accessibilityFrame]; + + // Exclude any visible annotation callout view. + if (self.calloutViewForSelectedAnnotation) + { + UIBezierPath *calloutViewPath = [UIBezierPath bezierPathWithRect:self.calloutViewForSelectedAnnotation.frame]; + [path appendPath:calloutViewPath]; + } + + return path; +} + - (NSInteger)accessibilityElementCount { + if (self.calloutViewForSelectedAnnotation) + { + return 2 /* selectedAnnotationCalloutView, mapViewProxyAccessibilityElement */; + } std::vector visibleAnnotations = [self annotationTagsInRect:self.bounds]; return visibleAnnotations.size() + 3 /* compass, userLocationAnnotationView, attributionButton */; } - (id)accessibilityElementAtIndex:(NSInteger)index { + if (self.calloutViewForSelectedAnnotation) + { + if (index == 0) + { + return self.calloutViewForSelectedAnnotation; + } + if (index == 1) + { + self.mapViewProxyAccessibilityElement.accessibilityFrame = self.accessibilityFrame; + self.mapViewProxyAccessibilityElement.accessibilityPath = self.accessibilityPath; + return self.mapViewProxyAccessibilityElement; + } + return nil; + } std::vector visibleAnnotations = [self annotationTagsInRect:self.bounds]; // Ornaments @@ -1881,6 +1952,11 @@ - (id)accessibilityElementAtIndex:(NSInteger)index - (NSInteger)indexOfAccessibilityElement:(id)element { + if (self.calloutViewForSelectedAnnotation) + { + return [@[self.calloutViewForSelectedAnnotation, self.mapViewProxyAccessibilityElement] + indexOfObject:element]; + } if (element == self.compassView) { return 0; @@ -1907,6 +1983,15 @@ - (NSInteger)indexOfAccessibilityElement:(id)element else return std::distance(visibleAnnotations.begin(), foundElement) + 2 /* compass, userLocationAnnotationView */; } +- (UIAccessibilityElement *)mapViewProxyAccessibilityElement +{ + if ( ! _mapViewProxyAccessibilityElement) + { + _mapViewProxyAccessibilityElement = [[MGLAnnotationAccessibilityElement alloc] initWithAccessibilityContainer:self]; + } + return _mapViewProxyAccessibilityElement; +} + #pragma mark - Geography - + (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCenterCoordinate @@ -3062,14 +3147,16 @@ - (void)selectAnnotation:(id )annotation animated:(BOOL)animated [self.delegate mapView:self annotationCanShowCallout:annotation]) { // build the callout + UIView *calloutView; if ([self.delegate respondsToSelector:@selector(mapView:calloutViewForAnnotation:)]) { - self.calloutViewForSelectedAnnotation = [self.delegate mapView:self calloutViewForAnnotation:annotation]; + calloutView = [self.delegate mapView:self calloutViewForAnnotation:annotation]; } - if (!self.calloutViewForSelectedAnnotation) + if (!calloutView) { - self.calloutViewForSelectedAnnotation = [self calloutViewForAnnotation:annotation]; + calloutView = [self calloutViewForAnnotation:annotation]; } + self.calloutViewForSelectedAnnotation = calloutView; if (_userLocationAnnotationIsSelected) { @@ -3084,41 +3171,38 @@ - (void)selectAnnotation:(id )annotation animated:(BOOL)animated // consult delegate for left and/or right accessory views if ([self.delegate respondsToSelector:@selector(mapView:leftCalloutAccessoryViewForAnnotation:)]) { - self.calloutViewForSelectedAnnotation.leftAccessoryView = - [self.delegate mapView:self leftCalloutAccessoryViewForAnnotation:annotation]; + calloutView.leftAccessoryView = [self.delegate mapView:self leftCalloutAccessoryViewForAnnotation:annotation]; - if ([self.calloutViewForSelectedAnnotation.leftAccessoryView isKindOfClass:[UIControl class]]) + if ([calloutView.leftAccessoryView isKindOfClass:[UIControl class]]) { UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleCalloutAccessoryTapGesture:)]; - [self.calloutViewForSelectedAnnotation.leftAccessoryView addGestureRecognizer:calloutAccessoryTap]; + [calloutView.leftAccessoryView addGestureRecognizer:calloutAccessoryTap]; } } if ([self.delegate respondsToSelector:@selector(mapView:rightCalloutAccessoryViewForAnnotation:)]) { - self.calloutViewForSelectedAnnotation.rightAccessoryView = - [self.delegate mapView:self rightCalloutAccessoryViewForAnnotation:annotation]; + calloutView.rightAccessoryView = [self.delegate mapView:self rightCalloutAccessoryViewForAnnotation:annotation]; - if ([self.calloutViewForSelectedAnnotation.rightAccessoryView isKindOfClass:[UIControl class]]) + if ([calloutView.rightAccessoryView isKindOfClass:[UIControl class]]) { UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleCalloutAccessoryTapGesture:)]; - [self.calloutViewForSelectedAnnotation.rightAccessoryView addGestureRecognizer:calloutAccessoryTap]; + [calloutView.rightAccessoryView addGestureRecognizer:calloutAccessoryTap]; } } // set annotation delegate to handle taps on the callout view - self.calloutViewForSelectedAnnotation.delegate = self; + calloutView.delegate = self; // present popup - [self.calloutViewForSelectedAnnotation presentCalloutFromRect:positioningRect - inView:self.glView - constrainedToView:self.glView - animated:animated]; - UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); + [calloutView presentCalloutFromRect:positioningRect + inView:self.glView + constrainedToView:self.glView + animated:animated]; } // notify delegate @@ -3200,8 +3284,6 @@ - (void)deselectAnnotation:(id )annotation animated:(BOOL)animate self.calloutViewForSelectedAnnotation = nil; self.selectedAnnotation = nil; - UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); - // notify delegate if ([self.delegate respondsToSelector:@selector(mapView:didDeselectAnnotation:)]) { @@ -3391,7 +3473,6 @@ - (void)setShowsUserLocation:(BOOL)showsUserLocation self.userLocationAnnotationView = [[MGLUserLocationAnnotationView alloc] initInMapView:self]; self.userLocationAnnotationView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin); - self.userLocationAnnotationView.isAccessibilityElement = YES; [self validateLocationServices]; } diff --git a/platform/ios/src/MGLUserLocationAnnotationView.m b/platform/ios/src/MGLUserLocationAnnotationView.m index 74908ec4e5e..d4f4a23fbde 100644 --- a/platform/ios/src/MGLUserLocationAnnotationView.m +++ b/platform/ios/src/MGLUserLocationAnnotationView.m @@ -57,6 +57,7 @@ - (instancetype)initInMapView:(MGLMapView *)mapView self.annotation = [[MGLUserLocation alloc] initWithMapView:mapView]; _mapView = mapView; [self setupLayers]; + self.isAccessibilityElement = YES; self.accessibilityTraits = UIAccessibilityTraitButton; _accessibilityCoordinateFormatter = [[MGLCoordinateFormatter alloc] init]; @@ -97,6 +98,11 @@ - (NSString *)accessibilityValue return [_accessibilityCoordinateFormatter stringFromCoordinate:self.mapView.centerCoordinate]; } +- (CGRect)accessibilityFrame +{ + return CGRectInset(self.frame, -15, -15); +} + - (UIBezierPath *)accessibilityPath { return [UIBezierPath bezierPathWithOvalInRect:self.frame]; From d9a2a845dec1697f6b68b6aa4c4a49b0c2738329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Sat, 16 May 2015 14:48:18 -0700 Subject: [PATCH 6/6] [ios] Added accessibility labels, hints, gestures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed an issue where the “return to map” accessibility element lacked a label. Always update the title and subtitle of an annotation accessibility element, in case the title or subtitle changes from one showing to the next. Added a hint for annotations. Use declarative rather than imperative for hints. Marked the map view and its annotations as adjustable so that swiping up and down with VoiceOver zooms out and in, respectively. Added accessibility values to toolbar buttons in iosapp. --- platform/ios/app/MBXViewController.m | 8 +- platform/ios/app/Main.storyboard | 15 +++- .../resources/Base.lproj/Localizable.strings | 5 +- platform/ios/src/MGLMapView.mm | 89 ++++++++++++++----- .../ios/src/MGLUserLocationAnnotationView.m | 13 ++- 5 files changed, 100 insertions(+), 30 deletions(-) diff --git a/platform/ios/app/MBXViewController.m b/platform/ios/app/MBXViewController.m index 098fc7b7441..196601d4949 100644 --- a/platform/ios/app/MBXViewController.m +++ b/platform/ios/app/MBXViewController.m @@ -518,24 +518,30 @@ - (IBAction)cycleStyles:(__unused id)sender [titleButton setTitle:styleNames[self.styleIndex] forState:UIControlStateNormal]; } -- (IBAction)locateUser:(__unused id)sender +- (IBAction)locateUser:(id)sender { MGLUserTrackingMode nextMode; + NSString *nextAccessibilityValue; switch (self.mapView.userTrackingMode) { case MGLUserTrackingModeNone: nextMode = MGLUserTrackingModeFollow; + nextAccessibilityValue = @"Follow location"; break; case MGLUserTrackingModeFollow: nextMode = MGLUserTrackingModeFollowWithHeading; + nextAccessibilityValue = @"Follow location and heading"; break; case MGLUserTrackingModeFollowWithHeading: nextMode = MGLUserTrackingModeFollowWithCourse; + nextAccessibilityValue = @"Follow course"; break; case MGLUserTrackingModeFollowWithCourse: nextMode = MGLUserTrackingModeNone; + nextAccessibilityValue = @"Off"; break; } self.mapView.userTrackingMode = nextMode; + [sender setAccessibilityValue:nextAccessibilityValue]; } - (IBAction)startWorldTour:(__unused id)sender diff --git a/platform/ios/app/Main.storyboard b/platform/ios/app/Main.storyboard index 72f9a02219f..1190070d8ea 100644 --- a/platform/ios/app/Main.storyboard +++ b/platform/ios/app/Main.storyboard @@ -38,7 +38,10 @@ - + + + + @@ -53,12 +56,18 @@ - + + + + - + + + + diff --git a/platform/ios/resources/Base.lproj/Localizable.strings b/platform/ios/resources/Base.lproj/Localizable.strings index 04cc667998c..d4475680728 100644 --- a/platform/ios/resources/Base.lproj/Localizable.strings +++ b/platform/ios/resources/Base.lproj/Localizable.strings @@ -1,3 +1,6 @@ +/* Accessibility hint */ +"ANNOTATION_A11Y_HINT" = "Shows more info"; + /* No comment provided by engineer. */ "API_CLIENT_400_DESC" = "The session data task failed. Original request was: %@"; @@ -29,7 +32,7 @@ "FIRST_STEPS_URL" = "mapbox.com/help/first-steps-ios-sdk"; /* Accessibility hint */ -"INFO_A11Y_HINT" = "Access credits, a feedback form, and more"; +"INFO_A11Y_HINT" = "Shows credits, a feedback form, and more"; /* Accessibility label */ "INFO_A11Y_LABEL" = "About this map"; diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index f6a0cb4904e..f2107eb3c97 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -133,10 +133,32 @@ @interface MGLAnnotationAccessibilityElement : UIAccessibilityElement @property (nonatomic) MGLAnnotationTag tag; +- (instancetype)initWithAccessibilityContainer:(id)container tag:(MGLAnnotationTag)identifier NS_DESIGNATED_INITIALIZER; + @end @implementation MGLAnnotationAccessibilityElement +- (instancetype)initWithAccessibilityContainer:(id)container tag:(MGLAnnotationTag)tag +{ + if (self = [super initWithAccessibilityContainer:container]) + { + _tag = tag; + self.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitAdjustable; + } + return self; +} + +- (void)accessibilityIncrement +{ + [self.accessibilityContainer accessibilityIncrement]; +} + +- (void)accessibilityDecrement +{ + [self.accessibilityContainer accessibilityDecrement]; +} + @end /// Lightweight container for metadata about an annotation, including the annotation itself. @@ -160,7 +182,7 @@ - (instancetype)initWithAccessibilityContainer:(id)container if (self = [super initWithAccessibilityContainer:container]) { self.accessibilityTraits = UIAccessibilityTraitButton; - self.accessibilityLabel = self.accessibilityLabel; + self.accessibilityLabel = [self.accessibilityContainer accessibilityLabel]; self.accessibilityHint = @"Returns to the map"; } return self; @@ -207,7 +229,7 @@ @interface MGLMapView () annotation = annotationContext.annotation; - MGLAnnotationAccessibilityElement *element = annotationContext.accessibilityElement; // Lazily create an accessibility element for the found annotation. - if ( ! element) + if ( ! annotationContext.accessibilityElement) { - element = [[MGLAnnotationAccessibilityElement alloc] initWithAccessibilityContainer:self]; - element.tag = annotationTag; - element.accessibilityTraits = UIAccessibilityTraitButton; - if ([annotation respondsToSelector:@selector(title)]) - { - element.accessibilityLabel = annotation.title; - } - if ([annotation respondsToSelector:@selector(subtitle)]) - { - element.accessibilityValue = annotation.subtitle; - } - annotationContext.accessibilityElement = element; + annotationContext.accessibilityElement = [[MGLAnnotationAccessibilityElement alloc] initWithAccessibilityContainer:self tag:annotationTag]; } - // Update the accessibility element’s frame. + // Update the accessibility element. MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag]; CGRect annotationFrame = [self frameOfImage:annotationImage.image centeredAtCoordinate:annotation.coordinate]; CGRect screenRect = UIAccessibilityConvertFrameToScreenCoordinates(annotationFrame, self); - element.accessibilityFrame = screenRect; + annotationContext.accessibilityElement.accessibilityFrame = screenRect; + annotationContext.accessibilityElement.accessibilityHint = NSLocalizedStringWithDefaultValue(@"ANNOTATION_A11Y_HINT", nil, nil, @"Shows more info", @"Accessibility hint"); - return element; + if ([annotation respondsToSelector:@selector(title)]) + { + annotationContext.accessibilityElement.accessibilityLabel = annotation.title; + } + if ([annotation respondsToSelector:@selector(subtitle)]) + { + annotationContext.accessibilityElement.accessibilityValue = annotation.subtitle; + } + + return annotationContext.accessibilityElement; } - (NSInteger)indexOfAccessibilityElement:(id)element @@ -1983,15 +2003,38 @@ - (NSInteger)indexOfAccessibilityElement:(id)element else return std::distance(visibleAnnotations.begin(), foundElement) + 2 /* compass, userLocationAnnotationView */; } -- (UIAccessibilityElement *)mapViewProxyAccessibilityElement +- (MGLMapViewProxyAccessibilityElement *)mapViewProxyAccessibilityElement { if ( ! _mapViewProxyAccessibilityElement) { - _mapViewProxyAccessibilityElement = [[MGLAnnotationAccessibilityElement alloc] initWithAccessibilityContainer:self]; + _mapViewProxyAccessibilityElement = [[MGLMapViewProxyAccessibilityElement alloc] initWithAccessibilityContainer:self]; } return _mapViewProxyAccessibilityElement; } +- (void)accessibilityIncrement +{ + // Swipe up to zoom out. + [self accessibilityScaleBy:0.5]; +} + +- (void)accessibilityDecrement +{ + // Swipe down to zoom in. + [self accessibilityScaleBy:2]; +} + +- (void)accessibilityScaleBy:(double)scaleFactor +{ + CGPoint centerPoint = self.contentCenter; + if (self.userTrackingMode != MGLUserTrackingModeNone) + { + centerPoint = self.userLocationAnnotationViewCenter; + } + _mbglMap->scaleBy(scaleFactor, mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); + [self unrotateIfNeededForGesture]; +} + #pragma mark - Geography - + (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCenterCoordinate diff --git a/platform/ios/src/MGLUserLocationAnnotationView.m b/platform/ios/src/MGLUserLocationAnnotationView.m index d4f4a23fbde..98b0c87bd25 100644 --- a/platform/ios/src/MGLUserLocationAnnotationView.m +++ b/platform/ios/src/MGLUserLocationAnnotationView.m @@ -57,8 +57,7 @@ - (instancetype)initInMapView:(MGLMapView *)mapView self.annotation = [[MGLUserLocation alloc] initWithMapView:mapView]; _mapView = mapView; [self setupLayers]; - self.isAccessibilityElement = YES; - self.accessibilityTraits = UIAccessibilityTraitButton; + self.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitAdjustable; _accessibilityCoordinateFormatter = [[MGLCoordinateFormatter alloc] init]; _accessibilityCoordinateFormatter.unitStyle = NSFormattingUnitStyleLong; @@ -108,6 +107,16 @@ - (UIBezierPath *)accessibilityPath return [UIBezierPath bezierPathWithOvalInRect:self.frame]; } +- (void)accessibilityIncrement +{ + [self.mapView accessibilityIncrement]; +} + +- (void)accessibilityDecrement +{ + [self.mapView accessibilityDecrement]; +} + - (void)setTintColor:(UIColor *)tintColor { if (_puckModeActivated)