From 5b488225fb720ef607c13a33ff6509d92bb7de95 Mon Sep 17 00:00:00 2001 From: Jason Wray Date: Thu, 11 Jul 2019 16:06:54 -0700 Subject: [PATCH] [ios] Add MGLCompassButton and MGLOrnamentVisibility --- platform/darwin/src/MGLTypes.h | 12 ++ platform/ios/CHANGELOG.md | 1 + platform/ios/ios.xcodeproj/project.pbxproj | 22 +++ platform/ios/jazzy.yml | 1 + platform/ios/sdk-files.json | 3 + platform/ios/src/MGLCompassButton.h | 22 +++ platform/ios/src/MGLCompassButton.mm | 128 ++++++++++++++++++ platform/ios/src/MGLCompassButton_Private.h | 19 +++ platform/ios/src/MGLMapView.h | 9 +- platform/ios/src/MGLMapView.mm | 105 ++------------ platform/ios/src/MGLMapView_Private.h | 3 + platform/ios/src/Mapbox.h | 1 + platform/ios/src/UIImage+MGLAdditions.h | 6 + platform/ios/src/UIImage+MGLAdditions.mm | 18 ++- platform/ios/test/MGLCompassButtonTests.m | 84 ++++++++++++ platform/ios/test/MGLMapViewDirectionTests.mm | 33 +++++ 16 files changed, 367 insertions(+), 100 deletions(-) create mode 100644 platform/ios/src/MGLCompassButton.h create mode 100644 platform/ios/src/MGLCompassButton.mm create mode 100644 platform/ios/src/MGLCompassButton_Private.h create mode 100644 platform/ios/test/MGLCompassButtonTests.m diff --git a/platform/darwin/src/MGLTypes.h b/platform/darwin/src/MGLTypes.h index 491e24310fe..b8354d2e83a 100644 --- a/platform/darwin/src/MGLTypes.h +++ b/platform/darwin/src/MGLTypes.h @@ -119,4 +119,16 @@ NS_INLINE MGLTransition MGLTransitionMake(NSTimeInterval duration, NSTimeInterva return transition; } +/** + Constants indicating the visibility of different map ornaments. + */ +typedef NS_ENUM(NSInteger, MGLOrnamentVisibility) { + /** A constant indicating that the ornament adapts to the current map state. */ + MGLOrnamentVisibilityAdaptive, + /** A constant indicating that the ornament is always hidden. */ + MGLOrnamentVisibilityHidden, + /** A constant indicating that the ornament is always visible. */ + MGLOrnamentVisibilityVisible +}; + NS_ASSUME_NONNULL_END diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md index cfdee6e8eba..884c966ba2f 100644 --- a/platform/ios/CHANGELOG.md +++ b/platform/ios/CHANGELOG.md @@ -34,6 +34,7 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT * `-[MGLMapView selectAnnotation:animated:completionHandler:]` * Deprecated variants of the above methods without completion handlers. ([#14959](https://github.com/mapbox/mapbox-gl-native/pull/14959)) * Fixed an issue where the two-finger tilt gesture would continue after lifting one finger. ([#14969](https://github.com/mapbox/mapbox-gl-native/pull/14969)) +* Added `MGLMapView.compassView.visibility` and `MGLOrnamentVisibility` to allow configuration of compass visibility behavior. ([#15055](https://github.com/mapbox/mapbox-gl-native/pull/15055)) ## 5.1.0 - June 19, 2019 diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj index fbf7974be0f..670e78a46c3 100644 --- a/platform/ios/ios.xcodeproj/project.pbxproj +++ b/platform/ios/ios.xcodeproj/project.pbxproj @@ -372,6 +372,8 @@ 9620BB3B1E69FE1700705A1D /* MGLSDKUpdateChecker.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9620BB371E69FE1700705A1D /* MGLSDKUpdateChecker.mm */; }; 9621F2502091020E005B3800 /* NSExpression+MGLAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 3510FFEE1D6D9D8C00F413B2 /* NSExpression+MGLAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 96381C0222C6F3950053497D /* MGLMapViewPitchTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 96381C0122C6F3950053497D /* MGLMapViewPitchTests.m */; }; + 9641771F22D546DA00332422 /* MGLCompassButton_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 9641771E22D546DA00332422 /* MGLCompassButton_Private.h */; }; + 9641772022D546DA00332422 /* MGLCompassButton_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 9641771E22D546DA00332422 /* MGLCompassButton_Private.h */; }; 9654C1261FFC1AB900DB6A19 /* MGLPolyline_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 9654C1251FFC1AB900DB6A19 /* MGLPolyline_Private.h */; }; 9654C1291FFC1CCD00DB6A19 /* MGLPolygon_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 9654C1271FFC1CC000DB6A19 /* MGLPolygon_Private.h */; }; 9658C155204761FC00D8A674 /* MGLMapViewScaleBarTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9658C154204761FC00D8A674 /* MGLMapViewScaleBarTests.m */; }; @@ -424,6 +426,11 @@ 96E516FB20005A4000A02306 /* MGLUserLocationHeadingBeamLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = 966FCF4A1F3A5C9200F2B6DE /* MGLUserLocationHeadingBeamLayer.h */; }; 96E516FC20005A4400A02306 /* MGLUserLocationHeadingIndicator.h in Headers */ = {isa = PBXBuildFile; fileRef = 96F3F73B1F5711F1003E2D2C /* MGLUserLocationHeadingIndicator.h */; }; 96E5170420005A6B00A02306 /* SMCalloutView.h in Headers */ = {isa = PBXBuildFile; fileRef = DA8848891CBB037E00AB86E3 /* SMCalloutView.h */; }; + 96E6145622CC135200109F14 /* MGLCompassButtonTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 96E6145522CC135200109F14 /* MGLCompassButtonTests.m */; }; + 96E6145922CC169000109F14 /* MGLCompassButton.h in Headers */ = {isa = PBXBuildFile; fileRef = 96E6145722CC169000109F14 /* MGLCompassButton.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 96E6145A22CC169000109F14 /* MGLCompassButton.h in Headers */ = {isa = PBXBuildFile; fileRef = 96E6145722CC169000109F14 /* MGLCompassButton.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 96E6145B22CC169000109F14 /* MGLCompassButton.mm in Sources */ = {isa = PBXBuildFile; fileRef = 96E6145822CC169000109F14 /* MGLCompassButton.mm */; }; + 96E6145C22CC169000109F14 /* MGLCompassButton.mm in Sources */ = {isa = PBXBuildFile; fileRef = 96E6145822CC169000109F14 /* MGLCompassButton.mm */; }; 96ED34DE22374C0900E9FCA9 /* MGLMapViewDirectionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 96ED34DD22374C0900E9FCA9 /* MGLMapViewDirectionTests.mm */; }; 96F3F73C1F57124B003E2D2C /* MGLUserLocationHeadingIndicator.h in Headers */ = {isa = PBXBuildFile; fileRef = 96F3F73B1F5711F1003E2D2C /* MGLUserLocationHeadingIndicator.h */; }; 9C188C4F2242C95A0022FA55 /* MMEDate.m in Sources */ = {isa = PBXBuildFile; fileRef = 40834BBC1FE05D6E00C1BD0D /* MMEDate.m */; }; @@ -1104,6 +1111,7 @@ 9620BB361E69FE1700705A1D /* MGLSDKUpdateChecker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLSDKUpdateChecker.h; sourceTree = ""; }; 9620BB371E69FE1700705A1D /* MGLSDKUpdateChecker.mm */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.objcpp; fileEncoding = 4; path = MGLSDKUpdateChecker.mm; sourceTree = ""; }; 96381C0122C6F3950053497D /* MGLMapViewPitchTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLMapViewPitchTests.m; sourceTree = ""; }; + 9641771E22D546DA00332422 /* MGLCompassButton_Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MGLCompassButton_Private.h; sourceTree = ""; }; 9654C1251FFC1AB900DB6A19 /* MGLPolyline_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLPolyline_Private.h; sourceTree = ""; }; 9654C1271FFC1CC000DB6A19 /* MGLPolygon_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLPolygon_Private.h; sourceTree = ""; }; 9658C154204761FC00D8A674 /* MGLMapViewScaleBarTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MGLMapViewScaleBarTests.m; sourceTree = ""; }; @@ -1132,6 +1140,9 @@ 96E0272C1E57C7E5004B8E66 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; 96E0272D1E57C7E6004B8E66 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; 96E0272E1E57C7E7004B8E66 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; + 96E6145522CC135200109F14 /* MGLCompassButtonTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MGLCompassButtonTests.m; sourceTree = ""; }; + 96E6145722CC169000109F14 /* MGLCompassButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MGLCompassButton.h; sourceTree = ""; }; + 96E6145822CC169000109F14 /* MGLCompassButton.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLCompassButton.mm; sourceTree = ""; }; 96ED34DD22374C0900E9FCA9 /* MGLMapViewDirectionTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLMapViewDirectionTests.mm; sourceTree = ""; }; 96F017292118FBAE00892778 /* MGLMapView_Experimental.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLMapView_Experimental.h; sourceTree = ""; }; 96F3F73B1F5711F1003E2D2C /* MGLUserLocationHeadingIndicator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MGLUserLocationHeadingIndicator.h; sourceTree = ""; }; @@ -1658,6 +1669,9 @@ 355ADFF91E9281C300F3939D /* Views */ = { isa = PBXGroup; children = ( + 96E6145722CC169000109F14 /* MGLCompassButton.h */, + 9641771E22D546DA00332422 /* MGLCompassButton_Private.h */, + 96E6145822CC169000109F14 /* MGLCompassButton.mm */, 355ADFFB1E9281DA00F3939D /* MGLScaleBar.h */, 355ADFFC1E9281DA00F3939D /* MGLScaleBar.mm */, ); @@ -2026,6 +2040,7 @@ DA35A2C31CCA9F8300E826B2 /* MGLClockDirectionFormatterTests.m */, 35D9DDE11DA25EEC00DAAD69 /* MGLCodingTests.mm */, DA35A2C41CCA9F8300E826B2 /* MGLCompassDirectionFormatterTests.m */, + 96E6145522CC135200109F14 /* MGLCompassButtonTests.m */, DA35A2A91CCA058D00E826B2 /* MGLCoordinateFormatterTests.m */, 3598544C1E1D38AA00B29F84 /* MGLDistanceFormatterTests.m */, 6407D66F1E0085FD00F6A9C3 /* MGLDocumentationExampleTests.swift */, @@ -2411,6 +2426,7 @@ DA35A2BB1CCA9A6900E826B2 /* MGLClockDirectionFormatter.h in Headers */, 353933FE1D3FB7DD003F57D7 /* MGLSymbolStyleLayer.h in Headers */, DA8848201CBAFA6200AB86E3 /* MGLOfflinePack_Private.h in Headers */, + 9641771F22D546DA00332422 /* MGLCompassButton_Private.h in Headers */, DA00FC8E1D5EEB0D009AABC8 /* MGLAttributionInfo.h in Headers */, DA8847FA1CBAFA5100AB86E3 /* MGLPolyline.h in Headers */, 3566C7711D4A9198008152BC /* MGLSource_Private.h in Headers */, @@ -2454,6 +2470,7 @@ 74CB5EC3219B282500102936 /* MGLCircleStyleLayer_Private.h in Headers */, DA8847F01CBAFA5100AB86E3 /* MGLAnnotation.h in Headers */, 400533011DB0862B0069F638 /* NSArray+MGLAdditions.h in Headers */, + 96E6145922CC169000109F14 /* MGLCompassButton.h in Headers */, 1F06668A1EC64F8E001C16D7 /* MGLLight.h in Headers */, 4049C29D1DB6CD6C00B3F799 /* MGLPointCollection.h in Headers */, 40CF6DBB1DAC3C6600A4D18B /* MGLShape_Private.h in Headers */, @@ -2602,6 +2619,7 @@ 96E516DE200054F700A02306 /* MGLGeometry_Private.h in Headers */, 353933FC1D3FB7C0003F57D7 /* MGLRasterStyleLayer.h in Headers */, 3566C76D1D4A8DFA008152BC /* MGLRasterTileSource.h in Headers */, + 9641772022D546DA00332422 /* MGLCompassButton_Private.h in Headers */, DAED38641D62D0FC00D7640F /* NSURL+MGLAdditions.h in Headers */, DABFB85E1CBE99E500D62B32 /* MGLAnnotation.h in Headers */, DABFB8641CBE99E500D62B32 /* MGLOfflineStorage.h in Headers */, @@ -2645,6 +2663,7 @@ 9C6E283922A982670056B7BE /* MMEEventLogReportViewController.h in Headers */, 1F6A82A92138871900BA5B41 /* MGLLoggingConfiguration_Private.h in Headers */, DABFB8661CBE99E500D62B32 /* MGLPointAnnotation.h in Headers */, + 96E6145A22CC169000109F14 /* MGLCompassButton.h in Headers */, 96E516E42000560B00A02306 /* MGLComputedShapeSource_Private.h in Headers */, 96E516E92000560B00A02306 /* MGLAnnotationImage_Private.h in Headers */, 96E516E52000560B00A02306 /* MGLOfflinePack_Private.h in Headers */, @@ -3240,6 +3259,7 @@ 40CFA6511D7875BB008103BD /* MGLShapeSourceTests.mm in Sources */, DA35A2C51CCA9F8300E826B2 /* MGLClockDirectionFormatterTests.m in Sources */, 35B8E08C1D6C8B5100E768D2 /* MGLPredicateTests.mm in Sources */, + 96E6145622CC135200109F14 /* MGLCompassButtonTests.m in Sources */, 96036A0620059BBA00510F3D /* MGLNSOrthographyAdditionsTests.m in Sources */, 1F95931D1E6DE2E900D5B294 /* MGLNSDateAdditionsTests.mm in Sources */, DA695426215B1E76002041A4 /* MGLMapCameraTests.m in Sources */, @@ -3336,6 +3356,7 @@ 3557F7B21E1D27D300CCA5E6 /* MGLDistanceFormatter.m in Sources */, 9C6E282B22A981570056B7BE /* MGLMapboxEvents.m in Sources */, 1FCCEC36222605C400302E3B /* MGLSDKMetricsManager.m in Sources */, + 96E6145B22CC169000109F14 /* MGLCompassButton.mm in Sources */, 40834BE71FE05E1800C1BD0D /* MMEAPIClient.m in Sources */, DA8848591CBAFB9800AB86E3 /* MGLMapView.mm in Sources */, DA8848501CBAFB9800AB86E3 /* MGLAnnotationImage.m in Sources */, @@ -3462,6 +3483,7 @@ 1FCCEC37222605C400302E3B /* MGLSDKMetricsManager.m in Sources */, 9C6E282C22A981580056B7BE /* MGLMapboxEvents.m in Sources */, DAA4E41F1CBB730400178DFB /* MGLMultiPoint.mm in Sources */, + 96E6145C22CC169000109F14 /* MGLCompassButton.mm in Sources */, DD0902AA1DB1929D00C5BDCE /* MGLNetworkConfiguration.m in Sources */, 40834C041FE05E1800C1BD0D /* MMELocationManager.m in Sources */, 9C6E281F22A980AC0056B7BE /* CLLocation+MMEMobileEvents.m in Sources */, diff --git a/platform/ios/jazzy.yml b/platform/ios/jazzy.yml index d459e936132..f5d0040ea5d 100644 --- a/platform/ios/jazzy.yml +++ b/platform/ios/jazzy.yml @@ -57,6 +57,7 @@ custom_categories: children: - MGLCalloutView - MGLCalloutViewDelegate + - MGLCompassButton - name: Location Updates children: - MGLLocationManager diff --git a/platform/ios/sdk-files.json b/platform/ios/sdk-files.json index fd2debd2e06..5ce2423ba97 100644 --- a/platform/ios/sdk-files.json +++ b/platform/ios/sdk-files.json @@ -64,6 +64,7 @@ "platform/darwin/src/MGLDistanceFormatter.m", "platform/ios/src/MGLMapboxEvents.m", "platform/darwin/src/MGLSDKMetricsManager.m", + "platform/ios/src/MGLCompassButton.mm", "platform/ios/vendor/mapbox-events-ios/MapboxMobileEvents/MMEAPIClient.m", "platform/ios/src/MGLMapView.mm", "platform/ios/src/MGLAnnotationImage.m", @@ -141,6 +142,7 @@ "MGLStyle.h": "platform/darwin/src/MGLStyle.h", "MGLUserLocationAnnotationView.h": "platform/ios/src/MGLUserLocationAnnotationView.h", "MGLAnnotation.h": "platform/darwin/src/MGLAnnotation.h", + "MGLCompassButton.h": "platform/ios/src/MGLCompassButton.h", "MGLLight.h": "platform/darwin/src/MGLLight.h", "MGLPointCollection.h": "platform/darwin/src/MGLPointCollection.h", "MGLAnnotationView.h": "platform/ios/src/MGLAnnotationView.h", @@ -195,6 +197,7 @@ "private_headers": { "MGLFillStyleLayer_Private.h": "platform/darwin/src/MGLFillStyleLayer_Private.h", "MGLOfflinePack_Private.h": "platform/darwin/src/MGLOfflinePack_Private.h", + "MGLCompassButton_Private.h": "platform/ios/src/MGLCompassButton_Private.h", "MGLSource_Private.h": "platform/darwin/src/MGLSource_Private.h", "MGLAnnotationView_Private.h": "platform/ios/src/MGLAnnotationView_Private.h", "MGLValueEvaluator.h": "platform/darwin/src/MGLValueEvaluator.h", diff --git a/platform/ios/src/MGLCompassButton.h b/platform/ios/src/MGLCompassButton.h new file mode 100644 index 00000000000..9c3d9d1c77c --- /dev/null +++ b/platform/ios/src/MGLCompassButton.h @@ -0,0 +1,22 @@ +#import + +#import "MGLTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + A specialized view that displays the current compass heading for its associated map. + */ +MGL_EXPORT +@interface MGLCompassButton : UIImageView + +/** + The visibility of the compass button. + + You can configure a compass button to be visible all the time or only when the compass heading changes. + */ +@property (nonatomic, assign) MGLOrnamentVisibility compassVisibility; + +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/ios/src/MGLCompassButton.mm b/platform/ios/src/MGLCompassButton.mm new file mode 100644 index 00000000000..94a6820a744 --- /dev/null +++ b/platform/ios/src/MGLCompassButton.mm @@ -0,0 +1,128 @@ +#import "MGLCompassButton_Private.h" +#import "MGLCompassDirectionFormatter.h" + +#import + +#import "MGLMapView_Private.h" +#import "UIImage+MGLAdditions.h" +#import "NSBundle+MGLAdditions.h" + +#include + +@interface MGLCompassButton () + +@property (nonatomic, weak) MGLMapView *mapView; +@property (nonatomic) MGLCompassDirectionFormatter *accessibilityCompassFormatter; + +@end + +@implementation MGLCompassButton + ++ (instancetype)compassButtonWithMapView:(MGLMapView *)mapView { + return [[MGLCompassButton alloc] initWithMapView:mapView]; +} + +- (instancetype)initWithMapView:(MGLMapView *)mapView { + if (self = [super init]) { + self.mapView = mapView; + [self commonInit]; + } + return self; +} + +- (void)commonInit { + self.image = self.compassImage; + + self.compassVisibility = MGLOrnamentVisibilityAdaptive; + + self.alpha = 0; + self.userInteractionEnabled = YES; + self.translatesAutoresizingMaskIntoConstraints = NO; + + UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; + [self addGestureRecognizer:tapGesture]; + + self.accessibilityTraits = UIAccessibilityTraitButton; + self.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"COMPASS_A11Y_LABEL", nil, nil, @"Compass", @"Accessibility label"); + self.accessibilityHint = NSLocalizedStringWithDefaultValue(@"COMPASS_A11Y_HINT", nil, nil, @"Rotates the map to face due north", @"Accessibility hint"); + + self.accessibilityCompassFormatter = [[MGLCompassDirectionFormatter alloc] init]; + self.accessibilityCompassFormatter.unitStyle = NSFormattingUnitStyleLong; + + [self sizeToFit]; +} + +- (void)setCompassVisibility:(MGLOrnamentVisibility)compassVisibility { + if (_compassVisibility == compassVisibility) { return; } + _compassVisibility = compassVisibility; + + [self updateCompassAnimated:NO]; +} + +- (UIImage *)compassImage { + UIImage *scaleImage = [UIImage mgl_resourceImageNamed:@"Compass"]; + UIGraphicsBeginImageContextWithOptions(scaleImage.size, NO, UIScreen.mainScreen.scale); + [scaleImage drawInRect:{CGPointZero, scaleImage.size}]; + + NSAttributedString *north = [[NSAttributedString alloc] initWithString:NSLocalizedStringWithDefaultValue(@"COMPASS_NORTH", nil, nil, @"N", @"Compass abbreviation for north") attributes:@{ + NSFontAttributeName: [UIFont systemFontOfSize:11 weight:UIFontWeightUltraLight], + NSForegroundColorAttributeName: [UIColor whiteColor], + }]; + CGRect stringRect = CGRectMake((scaleImage.size.width - north.size.width) / 2, + scaleImage.size.height * 0.435, + north.size.width, north.size.height); + [north drawInRect:stringRect]; + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} + +- (void)handleTapGesture:(__unused UITapGestureRecognizer *)sender { + [self.mapView resetNorth]; +} + +- (void)updateCompass { + [self updateCompassAnimated:YES]; +} + +- (void)updateCompassAnimated:(BOOL)animated { + CLLocationDirection direction = self.mapView.direction; + CLLocationDirection plateDirection = mbgl::util::wrap(-direction, 0., 360.); + self.transform = CGAffineTransformMakeRotation(MGLRadiansFromDegrees(plateDirection)); + + self.isAccessibilityElement = direction > 0; + self.accessibilityValue = [self.accessibilityCompassFormatter stringFromDirection:direction]; + + switch (self.compassVisibility) { + case MGLOrnamentVisibilityAdaptive: + if (direction > 0 && self.alpha < 1) { + [self showCompass:animated]; + } else if (direction == 0 && self.alpha > 0) { + [self hideCompass:animated]; + } + break; + case MGLOrnamentVisibilityVisible: + [self showCompass:animated]; + break; + case MGLOrnamentVisibilityHidden: + [self hideCompass:animated]; + break; + } +} + +- (void)showCompass:(BOOL)animated { + animated ? [self animateToAlpha:1] : [self setAlpha:1]; +} + +- (void)hideCompass:(BOOL)animated { + animated ? [self animateToAlpha:0] : [self setAlpha:0]; +} + +- (void)animateToAlpha:(CGFloat)alpha { + [UIView animateWithDuration:MGLAnimationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ + self.alpha = alpha; + } completion:nil]; +} + +@end diff --git a/platform/ios/src/MGLCompassButton_Private.h b/platform/ios/src/MGLCompassButton_Private.h new file mode 100644 index 00000000000..c9741d79e30 --- /dev/null +++ b/platform/ios/src/MGLCompassButton_Private.h @@ -0,0 +1,19 @@ +#import + +#import + +@class MGLMapView; + +NS_ASSUME_NONNULL_BEGIN + +@interface MGLCompassButton (Private) + ++ (instancetype)compassButtonWithMapView:(MGLMapView *)mapView; + +@property (nonatomic, weak) MGLMapView *mapView; + +- (void)updateCompass; + +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/ios/src/MGLMapView.h b/platform/ios/src/MGLMapView.h index 87c24cd97bc..9fe9a6c10a9 100644 --- a/platform/ios/src/MGLMapView.h +++ b/platform/ios/src/MGLMapView.h @@ -1,9 +1,9 @@ -#import "MGLGeometry.h" -#import "MGLMapCamera.h" - #import +#import "MGLCompassButton.h" #import "MGLFoundation.h" +#import "MGLGeometry.h" +#import "MGLMapCamera.h" #import "MGLTypes.h" NS_ASSUME_NONNULL_BEGIN @@ -122,7 +122,6 @@ FOUNDATION_EXTERN MGL_EXPORT const MGLMapViewPreferredFramesPerSecond MGLMapView FOUNDATION_EXTERN MGL_EXPORT MGLExceptionName const MGLMissingLocationServicesUsageDescriptionException; FOUNDATION_EXTERN MGL_EXPORT MGLExceptionName const MGLUserLocationAnnotationTypeException; -FOUNDATION_EXTERN MGL_EXPORT MGLExceptionName const MGLResourceNotFoundException; /** An interactive, customizable map view with an interface similar to the one @@ -316,7 +315,7 @@ MGL_EXPORT A control indicating the map’s direction and allowing the user to manipulate the direction, positioned in the upper-right corner. */ -@property (nonatomic, readonly) UIImageView *compassView; +@property (nonatomic, readonly) MGLCompassButton *compassView; /** The position of the compass view. The default value is `MGLOrnamentPositionTopRight`. diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index c0b73806079..d027f616bdc 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -55,11 +55,11 @@ #import "MGLUserLocation_Private.h" #import "MGLAnnotationImage_Private.h" #import "MGLAnnotationView_Private.h" +#import "MGLCompassButton_Private.h" #import "MGLScaleBar.h" #import "MGLStyle_Private.h" #import "MGLStyleLayer_Private.h" #import "MGLMapboxEvents.h" -#import "MMEConstants.h" #import "MGLSDKUpdateChecker.h" #import "MGLCompactCalloutView.h" #import "MGLAnnotationContainerView.h" @@ -68,6 +68,7 @@ #import "MGLMapAccessibilityElement.h" #import "MGLLocationManager_Private.h" #import "MGLLoggingConfiguration_Private.h" +#import "MMEConstants.h" #include #include @@ -86,7 +87,6 @@ const MGLExceptionName MGLMissingLocationServicesUsageDescriptionException = @"MGLMissingLocationServicesUsageDescriptionException"; const MGLExceptionName MGLUserLocationAnnotationTypeException = @"MGLUserLocationAnnotationTypeException"; -const MGLExceptionName MGLResourceNotFoundException = @"MGLResourceNotFoundException"; const MGLExceptionName MGLUnderlyingMapUnavailableException = @"MGLUnderlyingMapUnavailableException"; const CGPoint MGLOrnamentDefaultPositionOffset = CGPointMake(8, 8); @@ -197,7 +197,7 @@ @interface MGLMapView () *scaleBarConstraints; @property (nonatomic, readwrite) MGLScaleBar *scaleBar; -@property (nonatomic, readwrite) UIImageView *compassView; +@property (nonatomic, readwrite) MGLCompassButton *compassView; @property (nonatomic) NSMutableArray *compassViewConstraints; @property (nonatomic, readwrite) UIImageView *logoView; @property (nonatomic) NSMutableArray *logoViewConstraints; @@ -308,7 +308,6 @@ @implementation MGLMapView BOOL _delegateHasFillColorsForShapeAnnotations; BOOL _delegateHasLineWidthsForShapeAnnotations; - MGLCompassDirectionFormatter *_accessibilityCompassFormatter; NSArray> *_visiblePlaceFeatures; NSArray> *_visibleRoadFeatures; NSMutableSet *_featureAccessibilityElements; @@ -436,8 +435,6 @@ - (void)commonInit // self.isAccessibilityElement = YES; self.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"MAP_A11Y_LABEL", nil, nil, @"Map", @"Accessibility label"); self.accessibilityTraits = UIAccessibilityTraitAllowsDirectInteraction | UIAccessibilityTraitAdjustable; - _accessibilityCompassFormatter = [[MGLCompassDirectionFormatter alloc] init]; - _accessibilityCompassFormatter.unitStyle = NSFormattingUnitStyleLong; self.backgroundColor = [UIColor clearColor]; self.clipsToBounds = YES; if (@available(iOS 11.0, *)) { self.accessibilityIgnoresInvertColors = YES; } @@ -510,7 +507,7 @@ - (void)commonInit // setup logo // - UIImage *logo = [MGLMapView resourceImageNamed:@"mapbox"]; + UIImage *logo = [UIImage mgl_resourceImageNamed:@"mapbox"]; _logoView = [[UIImageView alloc] initWithImage:logo]; _logoView.accessibilityTraits = UIAccessibilityTraitStaticText; _logoView.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"LOGO_A11Y_LABEL", nil, nil, @"Mapbox", @"Accessibility label"); @@ -538,14 +535,7 @@ - (void)commonInit // setup compass // - _compassView = [[UIImageView alloc] initWithImage:self.compassImage]; - _compassView.alpha = 0; - _compassView.userInteractionEnabled = YES; - [_compassView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleCompassTapGesture:)]]; - _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"); - _compassView.translatesAutoresizingMaskIntoConstraints = NO; + _compassView = [MGLCompassButton compassButtonWithMapView:self]; [self addSubview:_compassView]; _compassViewConstraints = [NSMutableArray array]; _compassViewPosition = MGLOrnamentPositionTopRight; @@ -664,26 +654,6 @@ - (void)commonInit static_cast(size.height) }; } -- (UIImage *)compassImage -{ - UIImage *scaleImage = [MGLMapView resourceImageNamed:@"Compass"]; - UIGraphicsBeginImageContextWithOptions(scaleImage.size, NO, [UIScreen mainScreen].scale); - [scaleImage drawInRect:{ CGPointZero, scaleImage.size }]; - - NSAttributedString *north = [[NSAttributedString alloc] initWithString:NSLocalizedStringWithDefaultValue(@"COMPASS_NORTH", nil, nil, @"N", @"Compass abbreviation for north") attributes:@{ - NSFontAttributeName: [UIFont systemFontOfSize:11 weight:UIFontWeightUltraLight], - NSForegroundColorAttributeName: [UIColor whiteColor], - }]; - CGRect stringRect = CGRectMake((scaleImage.size.width - north.size.width) / 2, - scaleImage.size.height * 0.435, - north.size.width, north.size.height); - [north drawInRect:stringRect]; - - UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return image; -} - - (void)reachabilityChanged:(NSNotification *)notification { MGLAssertIsMainThread(); @@ -1522,19 +1492,6 @@ - (BOOL)canBecomeFirstResponder { #pragma mark - Gestures - -- (void)handleCompassTapGesture:(__unused id)sender -{ - self.cameraChangeReasonBitmask |= MGLCameraChangeReasonResetNorth; - - [self resetNorthAnimated:YES]; - - if (self.userTrackingMode == MGLUserTrackingModeFollowWithHeading || - self.userTrackingMode == MGLUserTrackingModeFollowWithCourse) - { - self.userTrackingMode = MGLUserTrackingModeFollow; - } -} - - (void)touchesBegan:(__unused NSSet *)touches withEvent:(__unused UIEvent *)event { if (!self.zoomEnabled && !self.pitchEnabled && !self.rotateEnabled && !self.scrollEnabled) @@ -2547,6 +2504,8 @@ - (void)resetNorth - (void)resetNorthAnimated:(BOOL)animated { + self.cameraChangeReasonBitmask |= MGLCameraChangeReasonResetNorth; + [self setDirection:0 animated:animated]; } @@ -3467,7 +3426,8 @@ - (void)setDirection:(CLLocationDirection)direction animated:(BOOL)animated MGLLogDebug(@"Setting direction: %f animated: %@", direction, MGLStringFromBOOL(animated)); if ( ! animated && ! self.rotationAllowed) return; - if (self.userTrackingMode == MGLUserTrackingModeFollowWithHeading) + if (self.userTrackingMode == MGLUserTrackingModeFollowWithHeading || + self.userTrackingMode == MGLUserTrackingModeFollowWithCourse) { self.userTrackingMode = MGLUserTrackingModeFollow; } @@ -4177,7 +4137,7 @@ - (void)updateAnnotationContainerViewWithAnnotationViews:(NSArray 0; - self.compassView.accessibilityValue = [_accessibilityCompassFormatter stringFromDirection:direction]; - - if (direction > 0 && self.compassView.alpha < 1) - { - [UIView animateWithDuration:MGLAnimationDuration - delay:0 - options:UIViewAnimationOptionBeginFromCurrentState - animations:^ - { - self.compassView.alpha = 1; - } - completion:nil]; - } - else if (direction == 0 && self.compassView.alpha > 0) - { - [UIView animateWithDuration:MGLAnimationDuration - delay:0 - options:UIViewAnimationOptionBeginFromCurrentState - animations:^ - { - self.compassView.alpha = 0; - } - completion:nil]; - } + [self.compassView updateCompass]; } - (void)updateScaleBar @@ -6593,21 +6525,6 @@ - (void)updateScaleBar } } -+ (UIImage *)resourceImageNamed:(NSString *)imageName -{ - UIImage *image = [UIImage imageNamed:imageName - inBundle:[NSBundle mgl_frameworkBundle] - compatibleWithTraitCollection:nil]; - - if ( ! image) - { - [NSException raise:MGLResourceNotFoundException format: - @"The resource named “%@” could not be found in the Mapbox framework bundle.", imageName]; - } - - return image; -} - - (BOOL)isFullyLoaded { return self.mbglMap.isFullyLoaded(); diff --git a/platform/ios/src/MGLMapView_Private.h b/platform/ios/src/MGLMapView_Private.h index 5aa4902a912..e53dc8519c0 100644 --- a/platform/ios/src/MGLMapView_Private.h +++ b/platform/ios/src/MGLMapView_Private.h @@ -12,6 +12,9 @@ namespace mbgl { class MGLMapViewImpl; @class MGLSource; +/// Standard animation duration for UI elements. +FOUNDATION_EXTERN const NSTimeInterval MGLAnimationDuration; + /// Minimum size of an annotation’s accessibility element. FOUNDATION_EXTERN const CGSize MGLAnnotationAccessibilityElementMinimumSize; diff --git a/platform/ios/src/Mapbox.h b/platform/ios/src/Mapbox.h index 635bda490f3..98e673577c9 100644 --- a/platform/ios/src/Mapbox.h +++ b/platform/ios/src/Mapbox.h @@ -15,6 +15,7 @@ FOUNDATION_EXPORT MGL_EXPORT const unsigned char MapboxVersionString[]; #import "MGLCalloutView.h" #import "MGLClockDirectionFormatter.h" #import "MGLCluster.h" +#import "MGLCompassButton.h" #import "MGLCompassDirectionFormatter.h" #import "MGLCoordinateFormatter.h" #import "MGLDistanceFormatter.h" diff --git a/platform/ios/src/UIImage+MGLAdditions.h b/platform/ios/src/UIImage+MGLAdditions.h index 22bb7402425..55835c50d6e 100644 --- a/platform/ios/src/UIImage+MGLAdditions.h +++ b/platform/ios/src/UIImage+MGLAdditions.h @@ -1,9 +1,13 @@ #import +#import + #include NS_ASSUME_NONNULL_BEGIN +FOUNDATION_EXTERN MGL_EXPORT MGLExceptionName const MGLResourceNotFoundException; + @interface UIImage (MGLAdditions) - (nullable instancetype)initWithMGLStyleImage:(const mbgl::style::Image *)styleImage; @@ -14,6 +18,8 @@ NS_ASSUME_NONNULL_BEGIN - (mbgl::PremultipliedImage)mgl_premultipliedImage; ++ (UIImage *)mgl_resourceImageNamed:(NSString *)imageName; + @end NS_ASSUME_NONNULL_END diff --git a/platform/ios/src/UIImage+MGLAdditions.mm b/platform/ios/src/UIImage+MGLAdditions.mm index 884f92e003d..9d05abd6ca3 100644 --- a/platform/ios/src/UIImage+MGLAdditions.mm +++ b/platform/ios/src/UIImage+MGLAdditions.mm @@ -1,7 +1,10 @@ #import "UIImage+MGLAdditions.h" +#import "NSBundle+MGLAdditions.h" #include +const MGLExceptionName MGLResourceNotFoundException = @"MGLResourceNotFoundException"; + @implementation UIImage (MGLAdditions) - (nullable instancetype)initWithMGLStyleImage:(const mbgl::style::Image *)styleImage @@ -42,7 +45,20 @@ - (nullable instancetype)initWithMGLPremultipliedImage:(const mbgl::Premultiplie float(self.scale), isTemplate); } --(mbgl::PremultipliedImage)mgl_premultipliedImage { +- (mbgl::PremultipliedImage)mgl_premultipliedImage { return MGLPremultipliedImageFromCGImage(self.CGImage); } + ++ (UIImage *)mgl_resourceImageNamed:(NSString *)imageName { + UIImage *image = [UIImage imageNamed:imageName + inBundle:[NSBundle mgl_frameworkBundle] + compatibleWithTraitCollection:nil]; + + if (!image) { + [NSException raise:MGLResourceNotFoundException format:@"The resource named “%@” could not be found in the Mapbox framework bundle.", imageName]; + } + + return image; +} + @end diff --git a/platform/ios/test/MGLCompassButtonTests.m b/platform/ios/test/MGLCompassButtonTests.m new file mode 100644 index 00000000000..f41de0a2e25 --- /dev/null +++ b/platform/ios/test/MGLCompassButtonTests.m @@ -0,0 +1,84 @@ +#import +#import + +#import "../../ios/src/MGLCompassButton_Private.h" + +@interface MGLMapView (MGLCompassButtonTests) + +- (void)resetNorthAnimated:(BOOL)animated; + +@end + +@interface MGLCompassButtonTests : XCTestCase + +@property (nonatomic) MGLMapView *mapView; + +@end + +@implementation MGLCompassButtonTests + +- (void)setUp { + [super setUp]; + + [MGLAccountManager setAccessToken:@"pk.feedcafedeadbeefbadebede"]; + NSURL *styleURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"one-liner" withExtension:@"json"]; + self.mapView = [[MGLMapView alloc] initWithFrame:UIScreen.mainScreen.bounds styleURL:styleURL]; +} + +- (void)tearDown { + self.mapView = nil; + [MGLAccountManager setAccessToken:nil]; + + [super tearDown]; +} + +- (void)testCompassButton { + XCTAssertNotNil(self.mapView.compassView); + XCTAssertTrue(self.mapView.compassView.userInteractionEnabled); + XCTAssertEqual(self.mapView.compassView.gestureRecognizers.count, 1); + XCTAssertEqual(self.mapView.compassView.accessibilityTraits, UIAccessibilityTraitButton); + XCTAssertNotNil(self.mapView.compassView.accessibilityLabel); + XCTAssertNotNil(self.mapView.compassView.accessibilityHint); +} + +- (void)testVisibilityAdaptive { + XCTAssertEqual(self.mapView.compassView.compassVisibility, MGLOrnamentVisibilityAdaptive, @"Adaptive should be the default visibility setting."); + XCTAssertEqual(self.mapView.compassView.alpha, 0, @"Compass should not initially be visible."); + + self.mapView.zoomLevel = 15.f; + [self.mapView setDirection:45.f animated:false]; + XCTAssertEqualWithAccuracy(self.mapView.direction, 45, 0.001); + XCTAssertEqual(self.mapView.compassView.alpha, 1, @"Compass should become visible when direction changes."); + + [self.mapView resetNorthAnimated:NO]; + XCTAssertEqual(self.mapView.direction, 0); + XCTAssertEqual(self.mapView.compassView.alpha, 0, @"Compass should hide itself when direction is north."); +} + +- (void)testVisibilityHidden { + self.mapView.compassView.compassVisibility = MGLOrnamentVisibilityHidden; + XCTAssertEqual(self.mapView.compassView.compassVisibility, MGLOrnamentVisibilityHidden); + XCTAssertEqual(self.mapView.compassView.alpha, 0, @"Compass should not initially be visible."); + + self.mapView.zoomLevel = 15.f; + [self.mapView setDirection:45.f animated:false]; + XCTAssertEqualWithAccuracy(self.mapView.direction, 45, 0.001); + XCTAssertEqual(self.mapView.compassView.alpha, 0, @"Compass should stay hidden when direction changes."); +} + +- (void)testVisibilityVisible { + self.mapView.compassView.compassVisibility = MGLOrnamentVisibilityVisible; + XCTAssertEqual(self.mapView.compassView.compassVisibility, MGLOrnamentVisibilityVisible); + XCTAssertEqual(self.mapView.compassView.alpha, 1, @"Compass should initially be visible."); + + self.mapView.zoomLevel = 15.f; + [self.mapView setDirection:45.f animated:false]; + XCTAssertEqualWithAccuracy(self.mapView.direction, 45, 0.001); + XCTAssertEqual(self.mapView.compassView.alpha, 1, @"Compass should continue to be visible when direction changes."); + + [self.mapView resetNorthAnimated:NO]; + XCTAssertEqual(self.mapView.direction, 0); + XCTAssertEqual(self.mapView.compassView.alpha, 1, @"Compass should continue to be visible when direction is north."); +} + +@end diff --git a/platform/ios/test/MGLMapViewDirectionTests.mm b/platform/ios/test/MGLMapViewDirectionTests.mm index 7f7e0903e3e..d3990b581ba 100644 --- a/platform/ios/test/MGLMapViewDirectionTests.mm +++ b/platform/ios/test/MGLMapViewDirectionTests.mm @@ -51,6 +51,39 @@ - (void)testDirection { XCTAssertEqual(mapView.direction, 0, @"Reset-to-north should set direction to 0°."); } +- (void)testRotateEnabled { + mapView.zoomLevel = 10; + + UIRotationGestureRecognizer *gesture = [[UIRotationGestureRecognizer alloc] initWithTarget:nil action:nil]; + gesture.state = UIGestureRecognizerStateBegan; + gesture.rotation = MGLRadiansFromDegrees(30); + CGFloat wrappedRotation = mbgl::util::wrap(-MGLDegreesFromRadians(gesture.rotation), 0., 360.); + + // Disabled + { + mapView.rotateEnabled = NO; + XCTAssertEqual(mapView.allowsRotating, NO); + + [mapView handleRotateGesture:gesture]; + XCTAssertNotEqual(mapView.direction, wrappedRotation, @"Gestural rotation should not work when rotation is disabled."); + + mapView.direction = 45.f; + XCTAssertEqualWithAccuracy(mapView.direction, 45, 0.001, @"Programmatic rotation is allowed when rotateEnabled = NO."); + } + + // Enabled + { + [mapView resetNorthAnimated:NO]; + mapView.rotateEnabled = YES; + XCTAssertEqual(mapView.allowsRotating, YES); + + gesture.state = UIGestureRecognizerStateChanged; + gesture.rotation = MGLRadiansFromDegrees(30); + [mapView handleRotateGesture:gesture]; + XCTAssertEqualWithAccuracy(mapView.direction, wrappedRotation, 0.001, @"Gestural rotation should work when rotation is enabled."); + } +} + - (void)testRotateGesture { mapView.zoomLevel = 15;