diff options
Diffstat (limited to 'platform/ios')
-rw-r--r-- | platform/ios/MGLAccountManager.m | 23 | ||||
-rw-r--r-- | platform/ios/MGLAccountManager_Private.h | 10 | ||||
-rw-r--r-- | platform/ios/MGLMapView.mm | 406 | ||||
-rw-r--r-- | platform/ios/MGLMapboxEvents.h | 1 | ||||
-rw-r--r-- | platform/ios/MGLMapboxEvents.m | 60 |
5 files changed, 274 insertions, 226 deletions
diff --git a/platform/ios/MGLAccountManager.m b/platform/ios/MGLAccountManager.m index a49433777d..71987786c9 100644 --- a/platform/ios/MGLAccountManager.m +++ b/platform/ios/MGLAccountManager.m @@ -1,6 +1,6 @@ #import <Foundation/Foundation.h> -#import "MGLAccountManager.h" +#import "MGLAccountManager_Private.h" #import "MGLMapboxEvents.h" #import "NSProcessInfo+MGLAdditions.h" @@ -14,6 +14,18 @@ @implementation MGLAccountManager ++ (void)load { + // Read the initial configuration from Info.plist. The shown-in-app setting + // preempts the Settings bundle check in -[MGLMapboxEvents init] triggered + // by setting the access token. + NSBundle *bundle = [NSBundle mainBundle]; + NSNumber *shownInAppNumber = [bundle objectForInfoDictionaryKey:@"MGLMapboxMetricsEnabledSettingShownInApp"]; + if (shownInAppNumber) { + [MGLAccountManager sharedManager].mapboxMetricsEnabledSettingShownInApp = [shownInAppNumber boolValue]; + } + self.accessToken = [bundle objectForInfoDictionaryKey:@"MGLMapboxAccessToken"]; +} + // Can be called from any thread. // + (instancetype) sharedManager { @@ -25,7 +37,6 @@ void (^setupBlock)() = ^{ dispatch_once(&onceToken, ^{ _sharedManager = [[self alloc] init]; - _sharedManager.mapboxMetricsEnabledSettingShownInApp = NO; }); }; if ( ! [[NSThread currentThread] isMainThread]) { @@ -39,16 +50,14 @@ return _sharedManager; } -+ (void) setMapboxMetricsEnabledSettingShownInApp:(BOOL)shown { - [MGLAccountManager sharedManager].mapboxMetricsEnabledSettingShownInApp = shown; -} - + (BOOL) mapboxMetricsEnabledSettingShownInApp { return [MGLAccountManager sharedManager].mapboxMetricsEnabledSettingShownInApp; } + (void) setAccessToken:(NSString *) accessToken { - [[MGLAccountManager sharedManager] setAccessToken:accessToken]; + if ( ! [accessToken length]) return; + + [MGLAccountManager sharedManager].accessToken = accessToken; // Update MGLMapboxEvents // NOTE: This is (likely) the initial setup of MGLMapboxEvents diff --git a/platform/ios/MGLAccountManager_Private.h b/platform/ios/MGLAccountManager_Private.h new file mode 100644 index 0000000000..3cc05b09f0 --- /dev/null +++ b/platform/ios/MGLAccountManager_Private.h @@ -0,0 +1,10 @@ +#import "MGLAccountManager.h" + +@interface MGLAccountManager (Private) + +/** Returns the shared instance of the `MGLAccountManager` class. */ ++ (instancetype)sharedManager; + +@property (atomic) NSString *accessToken; + +@end diff --git a/platform/ios/MGLMapView.mm b/platform/ios/MGLMapView.mm index 5d14107518..a517a4d26f 100644 --- a/platform/ios/MGLMapView.mm +++ b/platform/ios/MGLMapView.mm @@ -24,6 +24,7 @@ #import "MGLUserLocationAnnotationView.h" #import "MGLUserLocation_Private.h" #import "MGLFileCache.h" +#import "MGLAccountManager_Private.h" #import "MGLMapboxEvents.h" #import "SMCalloutView.h" @@ -35,7 +36,7 @@ class MBGLView; NSString *const MGLDefaultStyleName = @"mapbox-streets"; NSString *const MGLStyleVersion = @"7"; NSString *const MGLDefaultStyleMarkerSymbolName = @"default_marker"; -NSString *const MGLMapboxAccessTokenManagerURLDisplayString = @"mapbox.com/account/apps"; +NSString *const MGLMapboxSetupDocumentationURLDisplayString = @"mapbox.com/guides/first-steps-gl-ios"; const NSTimeInterval MGLAnimationDuration = 0.3; const CGSize MGLAnnotationUpdateViewportOutset = {150, 150}; @@ -48,6 +49,16 @@ static NSURL *MGLURLForBundledStyleNamed(NSString *styleName) return [NSURL URLWithString:[NSString stringWithFormat:@"asset://styles/%@.json", styleName]]; } +CGFloat MGLRadiansFromDegrees(CLLocationDegrees degrees) +{ + return degrees * M_PI / 180; +} + +CLLocationDegrees MGLDegreesFromRadians(CGFloat radians) +{ + return radians * 180 / M_PI; +} + #pragma mark - Private - @interface MGLMapView () <UIGestureRecognizerDelegate, GLKViewDelegate, CLLocationManagerDelegate, UIActionSheetDelegate> @@ -103,63 +114,48 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) - (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - - if (self && [self commonInit]) + if (self = [super initWithFrame:frame]) { + [self commonInit]; self.styleURL = nil; - self.accessToken = [MGLAccountManager accessToken]; - return self; } - - return nil; -} - -- (instancetype)initWithFrame:(CGRect)frame accessToken:(NSString *)accessToken -{ - return [self initWithFrame:frame accessToken:accessToken styleURL:nil]; + return self; } -- (instancetype)initWithFrame:(CGRect)frame accessToken:(NSString *)accessToken styleURL:(NSURL *)styleURL +- (instancetype)initWithFrame:(CGRect)frame styleURL:(NSURL *)styleURL { - self = [super initWithFrame:frame]; - - if (self && [self commonInit]) + if (self = [super initWithFrame:frame]) { - self.accessToken = accessToken; + [self commonInit]; self.styleURL = styleURL; } - return self; } - (instancetype)initWithCoder:(NSCoder *)decoder { - self = [super initWithCoder:decoder]; - - if (self && [self commonInit]) + if (self = [super initWithCoder:decoder]) { + [self commonInit]; self.styleURL = nil; - return self; } - - return nil; + return self; } - (NSString *)accessToken { - return @(_mbglMap->getAccessToken().c_str()).mgl_stringOrNilIfEmpty; + NSAssert(NO, @"-[MGLMapView accessToken] has been removed. Use +[MGLAccountManager accessToken] or get MGLMapboxAccessToken from the Info.plist."); + return nil; } - (void)setAccessToken:(NSString *)accessToken { - _mbglMap->setAccessToken(accessToken ? (std::string)[accessToken UTF8String] : ""); - [MGLAccountManager setAccessToken:accessToken.mgl_stringOrNilIfEmpty]; + NSAssert(NO, @"-[MGLMapView setAccessToken:] has been replaced by +[MGLAccountManager setAccessToken:].\n\nIf you previously set this access token in a storyboard inspectable, select the MGLMapView in Interface Builder and delete the “accessToken” entry from the User Defined Runtime Attributes section of the Identity inspector. Then go to the Info.plist file and set MGLMapboxAccessToken to “%@”.", accessToken); } + (NSSet *)keyPathsForValuesAffectingStyleURL { - return [NSSet setWithObjects:@"mapID", @"accessToken", nil]; + return [NSSet setWithObject:@"styleID"]; } - (NSURL *)styleURL @@ -188,20 +184,14 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) _mbglMap->setStyleURL([[styleURL absoluteString] UTF8String]); } -- (BOOL)commonInit +- (void)commonInit { _isTargetingInterfaceBuilder = NSProcessInfo.processInfo.mgl_isInterfaceBuilderDesignablesAgent; // create context // _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; - - if ( ! _context) - { - mbgl::Log::Error(mbgl::Event::Setup, "failed to create OpenGL ES context"); - - return NO; - } + NSAssert(_context, @"Failed to create OpenGL ES context."); // setup accessibility // @@ -253,13 +243,20 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) } _mbglMap->resize(self.bounds.size.width, self.bounds.size.height, _glView.contentScaleFactor); + // Observe for changes to the global access token (and find out the current one). + [[MGLAccountManager sharedManager] addObserver:self + forKeyPath:@"accessToken" + options:(NSKeyValueObservingOptionInitial | + NSKeyValueObservingOptionNew) + context:NULL]; + // Notify map object when network reachability status changes. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reachabilityChanged:) name:kReachabilityChangedNotification object:nil]; - Reachability* reachability = [Reachability reachabilityForInternetConnection]; + MGLReachability* reachability = [MGLReachability reachabilityForInternetConnection]; [reachability startNotifier]; // setup annotations @@ -294,6 +291,12 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) @"Improve This Map", nil]; + // iOS 8+: add action that opens app's Settings.app panel, if applicable + if (&UIApplicationOpenSettingsURLString != NULL && ! [MGLAccountManager mapboxMetricsEnabledSettingShownInApp]) + { + [_attributionSheet addButtonWithTitle:@"Adjust Privacy Settings"]; + } + // setup compass // _compass = [[UIImageView alloc] initWithImage:[MGLMapView resourceImageNamed:@"Compass.png"]]; @@ -376,13 +379,11 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) MGLEventKeyZoomLevel: @(zoom), MGLEventKeyPushEnabled: @([MGLMapboxEvents checkPushEnabled]) }]; - - return YES; } -(void)reachabilityChanged:(NSNotification*)notification { - Reachability *reachability = [notification object]; + MGLReachability *reachability = [notification object]; if ([reachability isReachable]) { mbgl::NetworkStatus::Reachable(); } @@ -393,6 +394,7 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) [_regionChangeDelegateQueue cancelAllOperations]; [[NSNotificationCenter defaultCenter] removeObserver:self]; + [[MGLAccountManager sharedManager] removeObserver:self forKeyPath:@"accessToken"]; if (_mbglMap) { @@ -905,13 +907,13 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) _mbglMap->setGestureInProgress(true); - self.angle = [MGLMapView degreesToRadians:_mbglMap->getBearing()] * -1; + self.angle = MGLRadiansFromDegrees(_mbglMap->getBearing()) * -1; self.userTrackingMode = MGLUserTrackingModeNone; } else if (rotate.state == UIGestureRecognizerStateChanged) { - CGFloat newDegrees = [MGLMapView radiansToDegrees:(self.angle + rotate.rotation)] * -1; + CGFloat newDegrees = MGLDegreesFromRadians(self.angle + rotate.rotation) * -1; // constrain to +/-30 degrees when merely rotating like Apple does // @@ -934,7 +936,7 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) CGFloat radians = self.angle + rotate.rotation; CGFloat duration = UIScrollViewDecelerationRateNormal; CGFloat newRadians = radians + velocity * duration * 0.1; - CGFloat newDegrees = [MGLMapView radiansToDegrees:newRadians] * -1; + CGFloat newDegrees = MGLDegreesFromRadians(newRadians) * -1; _mbglMap->setBearing(newDegrees, secondsAsDuration(duration)); @@ -999,16 +1001,16 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) mbgl::LatLngBounds tapBounds; coordinate = [self convertPoint:tapRectLowerLeft toCoordinateFromView:self]; - tapBounds.extend(coordinateToLatLng(coordinate)); + tapBounds.extend(MGLLatLngFromLocationCoordinate2D(coordinate)); coordinate = [self convertPoint:tapRectUpperLeft toCoordinateFromView:self]; - tapBounds.extend(coordinateToLatLng(coordinate)); + tapBounds.extend(MGLLatLngFromLocationCoordinate2D(coordinate)); coordinate = [self convertPoint:tapRectUpperRight toCoordinateFromView:self]; - tapBounds.extend(coordinateToLatLng(coordinate)); + tapBounds.extend(MGLLatLngFromLocationCoordinate2D(coordinate)); coordinate = [self convertPoint:tapRectLowerRight toCoordinateFromView:self]; - tapBounds.extend(coordinateToLatLng(coordinate)); + tapBounds.extend(MGLLatLngFromLocationCoordinate2D(coordinate)); // query for nearby annotations std::vector<uint32_t> nearbyAnnotations = _mbglMap->getAnnotationsInBounds(tapBounds); @@ -1270,10 +1272,25 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) [[UIApplication sharedApplication] openURL: [NSURL URLWithString:feedbackURL]]; } + // skips to 4 because button is conditionally added after cancel (index 3) + else if (buttonIndex == actionSheet.firstOtherButtonIndex + 4) + { + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]]; + } } #pragma mark - Properties - +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(__unused void *)context +{ + // Synchronize mbgl::Map’s access token with the global one in MGLAccountManager. + if ([keyPath isEqualToString:@"accessToken"] && object == [MGLAccountManager sharedManager]) + { + NSString *accessToken = change[NSKeyValueChangeNewKey]; + _mbglFileSource->setAccessToken(accessToken ? (std::string)[accessToken UTF8String] : ""); + } +} + + (NSSet *)keyPathsForValuesAffectingZoomEnabled { return [NSSet setWithObject:@"allowsZooming"]; @@ -1361,7 +1378,7 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) { CGFloat duration = (animated ? MGLAnimationDuration : 0); - _mbglMap->setLatLngZoom(coordinateToLatLng(coordinate), + _mbglMap->setLatLngZoom(MGLLatLngFromLocationCoordinate2D(coordinate), fmaxf(_mbglMap->getZoom(), self.currentMinimumZoom), secondsAsDuration(duration)); @@ -1375,7 +1392,7 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) - (CLLocationCoordinate2D)centerCoordinate { - return latLngToCoordinate(_mbglMap->getLatLng()); + return MGLLocationCoordinate2DFromLatLng(_mbglMap->getLatLng()); } - (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel animated:(BOOL)animated @@ -1384,7 +1401,7 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) CGFloat duration = (animated ? MGLAnimationDuration : 0); - _mbglMap->setLatLngZoom(coordinateToLatLng(centerCoordinate), zoomLevel, secondsAsDuration(duration)); + _mbglMap->setLatLngZoom(MGLLatLngFromLocationCoordinate2D(centerCoordinate), zoomLevel, secondsAsDuration(duration)); [self unrotateIfNeededAnimated:animated]; @@ -1468,12 +1485,12 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) // convertedPoint.y = self.bounds.size.height - convertedPoint.y; - return latLngToCoordinate(_mbglMap->latLngForPixel(mbgl::vec2<double>(convertedPoint.x, convertedPoint.y))); + return MGLLocationCoordinate2DFromLatLng(_mbglMap->latLngForPixel(mbgl::vec2<double>(convertedPoint.x, convertedPoint.y))); } - (CGPoint)convertCoordinate:(CLLocationCoordinate2D)coordinate toPointToView:(UIView *)view { - mbgl::vec2<double> pixel = _mbglMap->pixelForLatLng(coordinateToLatLng(coordinate)); + mbgl::vec2<double> pixel = _mbglMap->pixelForLatLng(MGLLatLngFromLocationCoordinate2D(coordinate)); // flip y coordinate for iOS view origin in top left // @@ -1487,12 +1504,12 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration) return _mbglMap->getMetersPerPixelAtLatitude(latitude, self.zoomLevel); } -mbgl::LatLng coordinateToLatLng(CLLocationCoordinate2D coordinate) +mbgl::LatLng MGLLatLngFromLocationCoordinate2D(CLLocationCoordinate2D coordinate) { return mbgl::LatLng(coordinate.latitude, coordinate.longitude); } -CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng) +CLLocationCoordinate2D MGLLocationCoordinate2DFromLatLng(mbgl::LatLng latLng) { return CLLocationCoordinate2DMake(latLng.latitude, latLng.longitude); } @@ -1501,13 +1518,13 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng) { mbgl::LatLngBounds bounds; - bounds.extend(coordinateToLatLng( + bounds.extend(MGLLatLngFromLocationCoordinate2D( [self convertPoint:CGPointMake(0, 0) toCoordinateFromView:self])); - bounds.extend(coordinateToLatLng( + bounds.extend(MGLLatLngFromLocationCoordinate2D( [self convertPoint:CGPointMake(self.bounds.size.width, 0) toCoordinateFromView:self])); - bounds.extend(coordinateToLatLng( + bounds.extend(MGLLatLngFromLocationCoordinate2D( [self convertPoint:CGPointMake(0, self.bounds.size.height) toCoordinateFromView:self])); - bounds.extend(coordinateToLatLng( + bounds.extend(MGLLatLngFromLocationCoordinate2D( [self convertPoint:CGPointMake(self.bounds.size.width, self.bounds.size.height) toCoordinateFromView:self])); return bounds; @@ -1533,27 +1550,31 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng) return [NSArray arrayWithArray:_bundledStyleURLs]; } -+ (NSSet *)keyPathsForValuesAffectingMapID ++ (NSSet *)keyPathsForValuesAffectingStyleID { - return [NSSet setWithObjects:@"styleURL", @"accessToken", nil]; + return [NSSet setWithObject:@"styleURL"]; } -- (NSString *)mapID +- (NSString *)styleID { NSURL *styleURL = self.styleURL; return [styleURL.scheme isEqualToString:@"mapbox"] ? styleURL.host.mgl_stringOrNilIfEmpty : nil; } +- (void)setStyleID:(NSString *)styleID +{ + self.styleURL = styleID ? [NSURL URLWithString:[NSString stringWithFormat:@"mapbox://%@", styleID]] : nil; +} + +- (NSString *)mapID +{ + NSAssert(NO, @"-[MGLMapView mapID] has been renamed -[MGLMapView styleID]."); + return nil; +} + - (void)setMapID:(NSString *)mapID { - if (mapID) - { - self.styleURL = [NSURL URLWithString:[NSString stringWithFormat:@"mapbox://%@", mapID]]; - } - else - { - self.styleURL = nil; - } + NSAssert(NO, @"-[MGLMapView setMapID:] has been renamed -[MGLMapView setStyleID:].\n\nIf you previously set this map ID in a storyboard inspectable, select the MGLMapView in Interface Builder and delete the “mapID” entry from the User Defined Runtime Attributes section of the Identity inspector. Then go to the Attributes inspector and enter “%@” into the “Style ID” field.", mapID); } - (NSArray *)styleClasses @@ -1657,7 +1678,7 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng) { assert([annotation conformsToProtocol:@protocol(MGLAnnotation)]); - latLngs.push_back(coordinateToLatLng(annotation.coordinate)); + latLngs.push_back(MGLLatLngFromLocationCoordinate2D(annotation.coordinate)); NSString *symbolName = nil; @@ -1726,7 +1747,7 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng) assert([firstAnnotation conformsToProtocol:@protocol(MGLAnnotation)]); - if ( ! [self viewportBounds].contains(coordinateToLatLng(firstAnnotation.coordinate))) return; + if ( ! [self viewportBounds].contains(MGLLatLngFromLocationCoordinate2D(firstAnnotation.coordinate))) return; [self selectAnnotation:firstAnnotation animated:NO]; } @@ -1735,7 +1756,7 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng) { if ( ! annotation) return; - if ( ! [self viewportBounds].contains(coordinateToLatLng(annotation.coordinate))) return; + if ( ! [self viewportBounds].contains(MGLLatLngFromLocationCoordinate2D(annotation.coordinate))) return; if (annotation == self.selectedAnnotation) return; @@ -2171,16 +2192,6 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng) #pragma mark - Utility - -+ (CGFloat)degreesToRadians:(CGFloat)degrees -{ - return degrees * M_PI / 180; -} - -+ (CGFloat)radiansToDegrees:(CGFloat)radians -{ - return radians * 180 / M_PI; -} - - (void)animateWithDelay:(NSTimeInterval)delay animations:(void (^)(void))animations { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), animations); @@ -2398,7 +2409,7 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng) while (degrees >= 360) degrees -= 360; while (degrees < 0) degrees += 360; - self.compass.transform = CGAffineTransformMakeRotation([MGLMapView degreesToRadians:degrees]); + self.compass.transform = CGAffineTransformMakeRotation(MGLRadiansFromDegrees(degrees)); if (_mbglMap->getBearing() && self.compass.alpha < 1) { @@ -2431,14 +2442,14 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng) imageName = [imageName stringByAppendingString:@".png"]; } - return [UIImage imageWithContentsOfFile:[MGLMapView pathForBundleResourceNamed:imageName ofType:nil inDirectory:@""]]; + return [UIImage imageWithContentsOfFile:[self pathForBundleResourceNamed:imageName ofType:nil inDirectory:@""]]; } + (NSString *)pathForBundleResourceNamed:(NSString *)name ofType:(NSString *)extension inDirectory:(NSString *)directory { NSString *path = [[NSBundle bundleWithPath:[NSBundle mgl_resourceBundlePath]] pathForResource:name ofType:extension inDirectory:directory]; - NSAssert(path, @"Resource not found in application."); + NSAssert(path, @"Resource %@ not found in application.", name); return path; } @@ -2461,129 +2472,106 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng) { [super prepareForInterfaceBuilder]; - self.layer.borderColor = [UIColor colorWithWhite:184/255. alpha:1].CGColor; - self.layer.borderWidth = 1; - - if (self.accessToken) - { - self.layer.backgroundColor = [UIColor colorWithRed:59/255. - green:178/255. - blue:208/255. - alpha:0.8].CGColor; - - UIImage *image = [[self class] resourceImageNamed:@"mapbox.png"]; - UIImageView *previewView = [[UIImageView alloc] initWithImage:image]; - previewView.translatesAutoresizingMaskIntoConstraints = NO; - [self addSubview:previewView]; - [self addConstraint: - [NSLayoutConstraint constraintWithItem:previewView - attribute:NSLayoutAttributeCenterXWithinMargins - relatedBy:NSLayoutRelationEqual - toItem:self - attribute:NSLayoutAttributeCenterXWithinMargins - multiplier:1 - constant:0]]; - [self addConstraint: - [NSLayoutConstraint constraintWithItem:previewView - attribute:NSLayoutAttributeCenterYWithinMargins - relatedBy:NSLayoutRelationEqual - toItem:self - attribute:NSLayoutAttributeCenterYWithinMargins - multiplier:1 - constant:0]]; - } - else - { - UIView *diagnosticView = [[UIView alloc] init]; - diagnosticView.translatesAutoresizingMaskIntoConstraints = NO; - [self addSubview:diagnosticView]; - - // Headline - UILabel *headlineLabel = [[UILabel alloc] init]; - headlineLabel.text = @"No Access Token"; - headlineLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]; - headlineLabel.textAlignment = NSTextAlignmentCenter; - headlineLabel.numberOfLines = 1; - headlineLabel.translatesAutoresizingMaskIntoConstraints = NO; - [headlineLabel setContentCompressionResistancePriority:UILayoutPriorityDefaultLow - forAxis:UILayoutConstraintAxisHorizontal]; - [diagnosticView addSubview:headlineLabel]; - - // Explanation - UILabel *explanationLabel = [[UILabel alloc] init]; - explanationLabel.text = @"To display a map here, you must provide a Mapbox access token. Get an access token from:"; - explanationLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; - explanationLabel.numberOfLines = 0; - explanationLabel.translatesAutoresizingMaskIntoConstraints = NO; - [explanationLabel setContentCompressionResistancePriority:UILayoutPriorityDefaultLow - forAxis:UILayoutConstraintAxisHorizontal]; - [diagnosticView addSubview:explanationLabel]; - - // Link - UIButton *linkButton = [UIButton buttonWithType:UIButtonTypeSystem]; - [linkButton setTitle:MGLMapboxAccessTokenManagerURLDisplayString forState:UIControlStateNormal]; - linkButton.translatesAutoresizingMaskIntoConstraints = NO; - [linkButton setContentCompressionResistancePriority:UILayoutPriorityDefaultLow - forAxis:UILayoutConstraintAxisHorizontal]; - [diagnosticView addSubview:linkButton]; - - // More explanation - UILabel *explanationLabel2 = [[UILabel alloc] init]; - explanationLabel2.text = @"and enter it into the Access Token field in the Attributes inspector."; - explanationLabel2.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; - explanationLabel2.numberOfLines = 0; - explanationLabel2.translatesAutoresizingMaskIntoConstraints = NO; - [explanationLabel2 setContentCompressionResistancePriority:UILayoutPriorityDefaultLow - forAxis:UILayoutConstraintAxisHorizontal]; - [diagnosticView addSubview:explanationLabel2]; - - // Constraints - NSDictionary *views = @{ - @"container": diagnosticView, - @"headline": headlineLabel, - @"explanation": explanationLabel, - @"link": linkButton, - @"explanation2": explanationLabel2, - }; - [self addConstraint: - [NSLayoutConstraint constraintWithItem:diagnosticView - attribute:NSLayoutAttributeCenterYWithinMargins - relatedBy:NSLayoutRelationEqual - toItem:self - attribute:NSLayoutAttributeCenterYWithinMargins - multiplier:1 - constant:0]]; - [self addConstraints: - [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[container(20@20)]-|" - options:NSLayoutFormatAlignAllCenterY - metrics:nil - views:views]]; - [self addConstraints: - [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[headline]-[explanation]-[link]-[explanation2]|" - options:0 - metrics:nil - views:views]]; - [self addConstraints: - [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[headline]|" - options:0 - metrics:nil - views:views]]; - [self addConstraints: - [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[explanation]|" - options:0 - metrics:nil - views:views]]; - [self addConstraints: - [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[link]|" - options:0 - metrics:nil - views:views]]; - [self addConstraints: - [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[explanation2]|" - options:0 - metrics:nil - views:views]]; - } + self.layer.borderColor = [UIColor colorWithRed:59/255. + green:178/255. + blue:208/255. + alpha:0.8].CGColor; + self.layer.borderWidth = 4; + self.layer.backgroundColor = [UIColor whiteColor].CGColor; + + UIView *diagnosticView = [[UIView alloc] init]; + diagnosticView.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:diagnosticView]; + + // Headline + UILabel *headlineLabel = [[UILabel alloc] init]; + headlineLabel.text = @"MGLMapView"; + headlineLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]; + headlineLabel.textAlignment = NSTextAlignmentCenter; + headlineLabel.numberOfLines = 1; + headlineLabel.translatesAutoresizingMaskIntoConstraints = NO; + [headlineLabel setContentCompressionResistancePriority:UILayoutPriorityDefaultLow + forAxis:UILayoutConstraintAxisHorizontal]; + [diagnosticView addSubview:headlineLabel]; + + // Explanation + UILabel *explanationLabel = [[UILabel alloc] init]; + explanationLabel.text = (@"To display a Mapbox-hosted map here:\n\n" + @"1. Set MGLMapboxAccessToken to your access token in Info.plist\n" + @"2. Add a Settings bundle that allows the user to turn Mapbox Metrics on and off\n\n" + @"For detailed instructions, see:"); + explanationLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; + explanationLabel.numberOfLines = 0; + explanationLabel.translatesAutoresizingMaskIntoConstraints = NO; + [explanationLabel setContentCompressionResistancePriority:UILayoutPriorityDefaultLow + forAxis:UILayoutConstraintAxisHorizontal]; + [diagnosticView addSubview:explanationLabel]; + + // Link + UIButton *linkButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [linkButton setTitle:MGLMapboxSetupDocumentationURLDisplayString forState:UIControlStateNormal]; + linkButton.translatesAutoresizingMaskIntoConstraints = NO; + linkButton.titleLabel.numberOfLines = 0; + [linkButton setContentCompressionResistancePriority:UILayoutPriorityDefaultLow + forAxis:UILayoutConstraintAxisHorizontal]; + [diagnosticView addSubview:linkButton]; + + // Constraints + NSDictionary *views = @{ + @"container": diagnosticView, + @"headline": headlineLabel, + @"explanation": explanationLabel, + @"link": linkButton, + }; + [self addConstraint: + [NSLayoutConstraint constraintWithItem:diagnosticView + attribute:NSLayoutAttributeCenterYWithinMargins + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeCenterYWithinMargins + multiplier:1 + constant:0]]; + [self addConstraint: + [NSLayoutConstraint constraintWithItem:diagnosticView + attribute:NSLayoutAttributeTopMargin + relatedBy:NSLayoutRelationGreaterThanOrEqual + toItem:self + attribute:NSLayoutAttributeTopMargin + multiplier:1 + constant:8]]; + [self addConstraint: + [NSLayoutConstraint constraintWithItem:self + attribute:NSLayoutAttributeBottomMargin + relatedBy:NSLayoutRelationGreaterThanOrEqual + toItem:diagnosticView + attribute:NSLayoutAttributeBottomMargin + multiplier:1 + constant:8]]; + [self addConstraints: + [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[container(20@20)]-|" + options:NSLayoutFormatAlignAllCenterY + metrics:nil + views:views]]; + [self addConstraints: + [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[headline]-[explanation]-[link]|" + options:0 + metrics:nil + views:views]]; + [self addConstraints: + [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[headline]|" + options:0 + metrics:nil + views:views]]; + [self addConstraints: + [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[explanation]|" + options:0 + metrics:nil + views:views]]; + [self addConstraints: + [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[link]|" + options:0 + metrics:nil + views:views]]; } class MBGLView : public mbgl::View diff --git a/platform/ios/MGLMapboxEvents.h b/platform/ios/MGLMapboxEvents.h index 94aa8be783..00de64a8b0 100644 --- a/platform/ios/MGLMapboxEvents.h +++ b/platform/ios/MGLMapboxEvents.h @@ -1,5 +1,6 @@ #import <Foundation/Foundation.h> +extern NSString *const MGLEventTypeAppUserTurnstile; extern NSString *const MGLEventTypeMapLoad; extern NSString *const MGLEventTypeMapTap; extern NSString *const MGLEventTypeMapDragEnd; diff --git a/platform/ios/MGLMapboxEvents.m b/platform/ios/MGLMapboxEvents.m index 98bfe21cfe..e7f400c5dc 100644 --- a/platform/ios/MGLMapboxEvents.m +++ b/platform/ios/MGLMapboxEvents.m @@ -13,9 +13,11 @@ #include <sys/sysctl.h> +static const NSUInteger version = 1; static NSString *const MGLMapboxEventsUserAgent = @"MapboxEventsiOS/1.0"; static NSString *MGLMapboxEventsAPIBase = @"https://api.tiles.mapbox.com"; +NSString *const MGLEventTypeAppUserTurnstile = @"appUserTurnstile"; NSString *const MGLEventTypeMapLoad = @"map.load"; NSString *const MGLEventTypeMapTap = @"map.click"; NSString *const MGLEventTypeMapDragEnd = @"map.dragend"; @@ -52,7 +54,6 @@ const NSTimeInterval MGLFlushInterval = 60; // All of the following properties are written to only from // the main thread, but can be read on any thread. // -@property (atomic) NSString *instanceID; @property (atomic) NSString *advertiserId; @property (atomic) NSString *vendorId; @property (atomic) NSString *model; @@ -66,8 +67,7 @@ const NSTimeInterval MGLFlushInterval = 60; - (instancetype)init { if (self = [super init]) { - _instanceID = [[NSUUID UUID] UUIDString]; - + // Dynamic detection of ASIdentifierManager from Mixpanel // https://github.com/mixpanel/mixpanel-iphone/blob/master/LICENSE _advertiserId = @""; @@ -143,6 +143,7 @@ const NSTimeInterval MGLFlushInterval = 60; @property (atomic) NSString *appName; @property (atomic) NSString *appVersion; @property (atomic) NSString *appBuildNumber; +@property (atomic) NSString *instanceID; @property (atomic) NSDateFormatter *rfc3339DateFormatter; @property (atomic) NSURLSession *session; @property (atomic) NSData *digicertCert; @@ -176,8 +177,10 @@ const NSTimeInterval MGLFlushInterval = 60; + (void)initialize { if (self == [MGLMapboxEvents class]) { + NSBundle *bundle = [NSBundle mainBundle]; + NSNumber *accountTypeNumber = [bundle objectForInfoDictionaryKey:@"MGLMapboxAccountType"]; [[NSUserDefaults standardUserDefaults] registerDefaults:@{ - @"MGLMapboxAccountType": @0, + @"MGLMapboxAccountType": accountTypeNumber ? accountTypeNumber : @0, @"MGLMapboxMetricsEnabled": @YES, }]; } @@ -195,7 +198,8 @@ const NSTimeInterval MGLFlushInterval = 60; self = [super init]; if (self) { - if (! [MGLAccountManager mapboxMetricsEnabledSettingShownInApp]) { + if (! [MGLAccountManager mapboxMetricsEnabledSettingShownInApp] && + [[NSUserDefaults standardUserDefaults] integerForKey:@"MGLMapboxAccountType"] == 0) { // Opt Out is not configured in UI, so check for Settings.bundle // Put Settings bundle into memory id defaultEnabledValue; @@ -213,7 +217,7 @@ const NSTimeInterval MGLFlushInterval = 60; } } - NSAssert(defaultEnabledValue, @"End users must be able to opt out of Metrics in your app, either inside Settings (via Settings.bundle) or inside this app. If you implement the opt-out control inside this app, disable this assertion by setting [MGLAccountManager setMapboxMetricsEnabledSettingShownInApp:YES] before the app initializes any Mapbox GL classes."); + NSAssert(defaultEnabledValue, @"End users must be able to opt out of Metrics in your app, either inside Settings (via Settings.bundle) or inside this app. If you implement the opt-out control inside this app, disable this assertion by setting MGLMapboxMetricsEnabledSettingShownInApp to YES in Info.plist."); [[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"MGLMapboxMetricsEnabled": defaultEnabledValue, }]; @@ -223,7 +227,8 @@ const NSTimeInterval MGLFlushInterval = 60; _appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"]; _appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; _appBuildNumber = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; - + _instanceID = [[NSUUID UUID] UUIDString]; + NSString *uniqueID = [[NSProcessInfo processInfo] globallyUniqueString]; _serialQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.events.serial", _appBundleId, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL); @@ -277,6 +282,11 @@ const NSTimeInterval MGLFlushInterval = 60; MGLMapboxEvents *strongSelf = weakSelf; [strongSelf validate]; }]; + + // Turn the Mapbox Turnstile to Count App Users + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + [self pushTurnstileEvent]; + }); } return self; } @@ -315,6 +325,7 @@ const NSTimeInterval MGLFlushInterval = 60; [[MGLMapboxEvents sharedManager] validate]; } +// Used to determine if Mapbox Metrics should be collected at any given point in time - (void)validate { MGLAssertIsMainThread(); BOOL enabledInSettings = [[self class] isEnabled]; @@ -414,6 +425,33 @@ const NSTimeInterval MGLFlushInterval = 60; } } +- (void) pushTurnstileEvent { + + __weak MGLMapboxEvents *weakSelf = self; + + dispatch_async(_serialQueue, ^{ + + MGLMapboxEvents *strongSelf = weakSelf; + + if ( ! strongSelf) return; + + // Build only IDFV event + NSString *vid = [[[UIDevice currentDevice] identifierForVendor] UUIDString]; + NSDictionary *vevt = @{@"event" : MGLEventTypeAppUserTurnstile, + @"created" : [strongSelf.rfc3339DateFormatter stringFromDate:[NSDate date]], + @"appBundleId" : strongSelf.appBundleId, + @"vendorId": vid, + @"version": @(version), + @"instance": strongSelf.instanceID}; + + // Add to Queue + [_eventQueue addObject:vevt]; + + // Flush + [strongSelf flush]; + }); +} + // Can be called from any thread. Can be called rapidly from // the UI thread, so performance is paramount. // @@ -430,9 +468,11 @@ const NSTimeInterval MGLFlushInterval = 60; __weak MGLMapboxEvents *weakSelf = self; dispatch_async(_serialQueue, ^{ + MGLMapboxEvents *strongSelf = weakSelf; + if ( ! strongSelf) return; - + // Metrics Collection Has Been Paused if (_paused) { return; @@ -443,9 +483,9 @@ const NSTimeInterval MGLFlushInterval = 60; NSMutableDictionary *evt = [[NSMutableDictionary alloc] initWithDictionary:attributeDictionary]; // mapbox-events stock attributes [evt setObject:event forKey:@"event"]; - [evt setObject:@(1) forKey:@"version"]; + [evt setObject:@(version) forKey:@"version"]; [evt setObject:[strongSelf.rfc3339DateFormatter stringFromDate:[NSDate date]] forKey:@"created"]; - [evt setObject:strongSelf.data.instanceID forKey:@"instance"]; + [evt setObject:strongSelf.instanceID forKey:@"instance"]; [evt setObject:strongSelf.data.advertiserId forKey:@"advertiserId"]; [evt setObject:strongSelf.data.vendorId forKey:@"vendorId"]; [evt setObject:strongSelf.appBundleId forKeyedSubscript:@"appBundleId"]; |