diff --git a/CMMapLauncher/CMMapLauncher.h b/CMMapLauncher/CMMapLauncher.h index e9169df..1c2fe64 100644 --- a/CMMapLauncher/CMMapLauncher.h +++ b/CMMapLauncher/CMMapLauncher.h @@ -51,6 +51,7 @@ typedef NS_ENUM(NSUInteger, CMMapApp) { CMMapAppTheTransitApp, // The Transit App CMMapAppWaze, // Waze CMMapAppYandex, // Yandex Navigator + CMMapAppUserPick // User can pick from a list of available applications. Needs to be last enum option. }; @interface CMMapLauncher : NSObject diff --git a/CMMapLauncher/CMMapLauncher.m b/CMMapLauncher/CMMapLauncher.m index ee53704..5d76617 100644 --- a/CMMapLauncher/CMMapLauncher.m +++ b/CMMapLauncher/CMMapLauncher.m @@ -22,16 +22,39 @@ #import "CMMapLauncher.h" -@interface CMMapLauncher () +@interface CMMapLauncher () + (NSString *)urlPrefixForMapApp:(CMMapApp)mapApp; + (NSString *)urlEncode:(NSString *)queryParam; + (NSString *)googleMapsStringForMapPoint:(CMMapPoint *)mapPoint; +@property (nonatomic, strong) CMMapPoint *start, *end; @end @implementation CMMapLauncher ++ (CMMapLauncher *)alertViewWatcherFor:(CMMapPoint *)start to:(CMMapPoint *)end +{ + static CMMapLauncher *sAlertViewWatcher = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sAlertViewWatcher = [[CMMapLauncher alloc] init]; + }); + sAlertViewWatcher.start = start; + sAlertViewWatcher.end = end; + return sAlertViewWatcher; +} + +- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { + if(buttonIndex == alertView.cancelButtonIndex) return; + NSString *name = [alertView buttonTitleAtIndex:buttonIndex]; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + CMMapApp app = [CMMapLauncher mapAppForName:name]; + [defaults setObject:@(app) forKey:@"CMMapAppUserPick"]; + [defaults synchronize]; + [CMMapLauncher launchMapApp:app forDirectionsFrom:self.start to:self.end]; +} + + (NSString *)urlPrefixForMapApp:(CMMapApp)mapApp { switch (mapApp) { case CMMapAppCitymapper: @@ -57,6 +80,39 @@ + (NSString *)urlPrefixForMapApp:(CMMapApp)mapApp { } } +// Nicer ways to do this in swift :) ++ (NSString *)nameForMapApp:(CMMapApp)mapApp { + switch (mapApp) { + case CMMapAppCitymapper: + return @"City Mapper"; + case CMMapAppGoogleMaps: + return @"Google Maps"; + case CMMapAppAppleMaps: + return @"Apple Maps"; + case CMMapAppUserPick: + return nil; + case CMMapAppNavigon: + return @"Navigon"; + case CMMapAppTheTransitApp: + return @"The Transit App"; + case CMMapAppWaze: + return @"Waze"; + case CMMapAppYandex: + return @"Yandex"; + } +} + ++ (CMMapApp)mapAppForName:(NSString *)name { + if([name isEqualToString:@"City Mapper"]) return CMMapAppCitymapper; + if([name isEqualToString:@"Google Maps"]) return CMMapAppGoogleMaps; + if([name isEqualToString:@"Apple Maps"]) return CMMapAppAppleMaps; + if([name isEqualToString:@"Navigon"]) return CMMapAppNavigon; + if([name isEqualToString:@"The Transit App"]) return CMMapAppTheTransitApp; + if([name isEqualToString:@"Waze"]) return CMMapAppWaze; + if([name isEqualToString:@"Yandex"]) return CMMapAppYandex; + return CMMapAppUserPick; +} + + (NSString *)urlEncode:(NSString *)queryParam { // Encode all the reserved characters, per RFC 3986 // () @@ -86,7 +142,15 @@ + (NSString *)googleMapsStringForMapPoint:(CMMapPoint *)mapPoint { } + (BOOL)isMapAppInstalled:(CMMapApp)mapApp { - if (mapApp == CMMapAppAppleMaps) { + BOOL canOpenDeprecated = ([[[UIDevice currentDevice] systemVersion] compare:@"9.0" options:NSNumericSearch] != NSOrderedAscending); + + if(canOpenDeprecated) + { + // iOS 9.0 deprecated canOpenURL, so we have no way to guess. This means we just have to assume yes. + return YES; + } + + if (mapApp == CMMapAppAppleMaps || mapApp == CMMapAppUserPick) { return YES; } @@ -102,13 +166,50 @@ + (BOOL)launchMapApp:(CMMapApp)mapApp forDirectionsTo:(CMMapPoint *)end { return [CMMapLauncher launchMapApp:mapApp forDirectionsFrom:[CMMapPoint currentLocation] to:end]; } ++ (void)clearDefaultMapApp { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + if([defaults objectForKey:@"CMMapAppUserPick"]) { + [defaults removeObjectForKey:@"CMMapAppUserPick"]; + [defaults synchronize]; + } +} + + (BOOL)launchMapApp:(CMMapApp)mapApp forDirectionsFrom:(CMMapPoint *)start to:(CMMapPoint *)end { if (![CMMapLauncher isMapAppInstalled:mapApp]) { + [self clearDefaultMapApp]; return NO; } + if (mapApp == CMMapAppUserPick) { + NSNumber *userPick = [[NSUserDefaults standardUserDefaults] objectForKey:@"CMMapAppUserPick"]; + if([userPick isKindOfClass:[NSNumber class]]) { + mapApp = userPick.integerValue; + } + else + { + UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Open with:" + message:nil + delegate:[self alertViewWatcherFor:start to:end] + cancelButtonTitle:nil + otherButtonTitles:nil]; + for (NSUInteger x = 0; x < CMMapAppUserPick; x++) { + if([self isMapAppInstalled:x]) { + [alertView addButtonWithTitle:[self nameForMapApp:x]]; + mapApp = x; + } + } + if(alertView.numberOfButtons > 1) { + // There is a choice. + [alertView addButtonWithTitle:@"Cancel"]; + alertView.cancelButtonIndex = alertView.numberOfButtons-1; + [alertView show]; + return YES; + } + } + } + if (mapApp == CMMapAppAppleMaps) { // Check for iOS 6 Class mapItemClass = [MKMapItem class]; @@ -127,7 +228,9 @@ + (BOOL)launchMapApp:(CMMapApp)mapApp [CMMapLauncher googleMapsStringForMapPoint:start], [CMMapLauncher googleMapsStringForMapPoint:end] ]; - return [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; + BOOL success = [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; + if(!success) [self clearDefaultMapApp]; + return success; } else if (mapApp == CMMapAppCitymapper) { NSMutableArray *params = [NSMutableArray arrayWithCapacity:10]; if (start && !start.isCurrentLocation) { @@ -149,7 +252,9 @@ + (BOOL)launchMapApp:(CMMapApp)mapApp } } NSString *url = [NSString stringWithFormat:@"citymapper://directions?%@", [params componentsJoinedByString:@"&"]]; - return [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; + BOOL success = [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; + if(!success) [self clearDefaultMapApp]; + return success; } else if (mapApp == CMMapAppTheTransitApp) { // http://thetransitapp.com/developers @@ -161,7 +266,9 @@ + (BOOL)launchMapApp:(CMMapApp)mapApp [params addObject:[NSString stringWithFormat:@"to=%f,%f", end.coordinate.latitude, end.coordinate.longitude]]; } NSString *url = [NSString stringWithFormat:@"transit://directions?%@", [params componentsJoinedByString:@"&"]]; - return [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; + BOOL success = [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; + if(!success) [self clearDefaultMapApp]; + return success; } else if (mapApp == CMMapAppNavigon) { // http://www.navigon.com/portal/common/faq/files/NAVIGON_AppInteract.pdf @@ -170,10 +277,14 @@ + (BOOL)launchMapApp:(CMMapApp)mapApp name = end.name; } NSString *url = [NSString stringWithFormat:@"navigon://coordinate/%@/%f/%f", [CMMapLauncher urlEncode:name], end.coordinate.longitude, end.coordinate.latitude]; - return [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; + BOOL success = [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; + if(!success) [self clearDefaultMapApp]; + return success; } else if (mapApp == CMMapAppWaze) { NSString *url = [NSString stringWithFormat:@"waze://?ll=%f,%f&navigate=yes", end.coordinate.latitude, end.coordinate.longitude]; - return [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; + BOOL success = [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; + if(!success) [self clearDefaultMapApp]; + return success; } else if (mapApp == CMMapAppYandex) { NSString *url = nil; if (start.isCurrentLocation) { @@ -181,7 +292,9 @@ + (BOOL)launchMapApp:(CMMapApp)mapApp } else { url = [NSString stringWithFormat:@"yandexnavi://build_route_on_map?lat_to=%f&lon_to=%f&lat_from=%f&lon_from=%f", end.coordinate.latitude, end.coordinate.longitude, start.coordinate.latitude, start.coordinate.longitude]; } - return [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; + BOOL success = [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; + if(!success) [self clearDefaultMapApp]; + return success; } return NO;