Open an Alert asking to choose App to open map with
Asked Answered
F

6

18

I have a view controller with map kit integrated. I need to shoot an alert before opening that map, asking to choose from all similar applications of maps to open it with. For instance, if google maps app is installed in my iPhone, there should be an option for it, along with the default mapkit view. Is there a possibility to achieve this functionality which scans every similar app from iphone and returns the result as options to open map with.

Fastness answered 7/7, 2016 at 16:10 Comment(0)
T
13

Swift 5+ solution based on previous answers, this one shows a selector between Apple Maps, Google Maps, Waze and City Mapper. It also allows for some optional location title (for those apps that support it) and presents the alert only if there are more than 1 option (it opens automatically if only 1, or does nothing if none).

func openMaps(latitude: Double, longitude: Double, title: String?) {
    let application = UIApplication.shared
    let coordinate = "\(latitude),\(longitude)"
    let encodedTitle = title?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
    let handlers = [
        ("Apple Maps", "http://maps.apple.com/?q=\(encodedTitle)&ll=\(coordinate)"),
        ("Google Maps", "comgooglemaps://?q=\(coordinate)"),
        ("Waze", "waze://?ll=\(coordinate)"),
        ("Citymapper", "citymapper://directions?endcoord=\(coordinate)&endname=\(encodedTitle)")
    ]
        .compactMap { (name, address) in URL(string: address).map { (name, $0) } }
        .filter { (_, url) in application.canOpenURL(url) }

    guard handlers.count > 1 else {
        if let (_, url) = handlers.first {
            application.open(url, options: [:])
        }
        return
    }
    let alert = UIAlertController(title: R.string.localizable.select_map_app(), message: nil, preferredStyle: .actionSheet)
    handlers.forEach { (name, url) in
        alert.addAction(UIAlertAction(title: name, style: .default) { _ in
            application.open(url, options: [:])
        })
    }
    alert.addAction(UIAlertAction(title: R.string.localizable.cancel(), style: .cancel, handler: nil))
    contextProvider.currentViewController.present(alert, animated: true, completion: nil)
}

Note this solution uses R.swift for string localization but you can replace those with NSLocalizedString normally, and it uses a contextProvider.currentViewController to get the presented UIViewController, but you can replace it with self if you are calling this in a view controller already.

As usual, you need to also add the following to your app Info.plist:

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>citymapper</string>
    <string>comgooglemaps</string>
    <string>waze</string>
</array>
Tomkin answered 28/4, 2020 at 11:19 Comment(0)
P
36

You can create an array of checks to map the installed apps using sumesh's answer [1]:

var installedNavigationApps : [String] = ["Apple Maps"] // Apple Maps is always installed

and with every navigation app you can think of:

if (UIApplication.sharedApplication().canOpenURL(url: NSURL)) {
        self.installedNavigationApps.append(url)
} else {
        // do nothing
}

Common navigation apps are:

  • Google Maps - NSURL(string:"comgooglemaps://")
  • Waze - NSURL(string:"waze://")
  • Navigon - NSURL(string:"navigon://")
  • TomTom - NSURL(string:"tomtomhome://")

A lot more can be found at: http://wiki.akosma.com/IPhone_URL_Schemes

After you created your list of installed navigation apps you can present an UIAlertController:

let alert = UIAlertController(title: "Selection", message: "Select Navigation App", preferredStyle: .ActionSheet)
for app in self.installNavigationApps {
    let button = UIAlertAction(title: app, style: .Default, handler: nil)
    alert.addAction(button)
}
self.presentViewController(alert, animated: true, completion: nil)

Of course you need to add the behavior of a button click in the handler with the specified urlscheme. For example if Google Maps is clicked use something like this:

UIApplication.sharedApplication().openURL(NSURL(string:
            "comgooglemaps://?saddr=&daddr=\(place.latitude),\(place.longitude)&directionsmode=driving")!) // Also from sumesh's answer

With only Apple Maps and Google Maps installed this will yield something like this:

enter image description here

Pirnot answered 7/7, 2016 at 17:7 Comment(3)
Check out github.com/zirinisp/PazMapDirections . I am currently working on it. Please remember to add schemes LSApplicationQueriesSchemes in your info.plistAustralasia
The link is deadRemscheid
github.com/zirinisp/PazMapDirections, this is good option in this case, although it's swift 3, but it was easy to convert to Swift 5. Thanks, @AustralasiaPractice
D
28

Swift 5+

Base on @Emptyless answer.

to

import MapKit

func openMapButtonAction() {
        let latitude = 45.5088
        let longitude = -73.554

        let appleURL = "http://maps.apple.com/?daddr=\(latitude),\(longitude)"
        let googleURL = "comgooglemaps://?daddr=\(latitude),\(longitude)&directionsmode=driving"
        let wazeURL = "waze://?ll=\(latitude),\(longitude)&navigate=false"

        let googleItem = ("Google Map", URL(string:googleURL)!)
        let wazeItem = ("Waze", URL(string:wazeURL)!)
        var installedNavigationApps = [("Apple Maps", URL(string:appleURL)!)]

        if UIApplication.shared.canOpenURL(googleItem.1) {
            installedNavigationApps.append(googleItem)
        }

        if UIApplication.shared.canOpenURL(wazeItem.1) {
            installedNavigationApps.append(wazeItem)
        }

        let alert = UIAlertController(title: "Selection", message: "Select Navigation App", preferredStyle: .actionSheet)
        for app in installedNavigationApps {
            let button = UIAlertAction(title: app.0, style: .default, handler: { _ in
                UIApplication.shared.open(app.1, options: [:], completionHandler: nil)
            })
            alert.addAction(button)
        }
        let cancel = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
        alert.addAction(cancel)
        present(alert, animated: true)
    }

Also put these in your info.plist:

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>googlechromes</string>
    <string>comgooglemaps</string>
    <string>waze</string>
</array>

Cheers!

Duple answered 30/3, 2020 at 12:23 Comment(0)
T
13

Swift 5+ solution based on previous answers, this one shows a selector between Apple Maps, Google Maps, Waze and City Mapper. It also allows for some optional location title (for those apps that support it) and presents the alert only if there are more than 1 option (it opens automatically if only 1, or does nothing if none).

func openMaps(latitude: Double, longitude: Double, title: String?) {
    let application = UIApplication.shared
    let coordinate = "\(latitude),\(longitude)"
    let encodedTitle = title?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
    let handlers = [
        ("Apple Maps", "http://maps.apple.com/?q=\(encodedTitle)&ll=\(coordinate)"),
        ("Google Maps", "comgooglemaps://?q=\(coordinate)"),
        ("Waze", "waze://?ll=\(coordinate)"),
        ("Citymapper", "citymapper://directions?endcoord=\(coordinate)&endname=\(encodedTitle)")
    ]
        .compactMap { (name, address) in URL(string: address).map { (name, $0) } }
        .filter { (_, url) in application.canOpenURL(url) }

    guard handlers.count > 1 else {
        if let (_, url) = handlers.first {
            application.open(url, options: [:])
        }
        return
    }
    let alert = UIAlertController(title: R.string.localizable.select_map_app(), message: nil, preferredStyle: .actionSheet)
    handlers.forEach { (name, url) in
        alert.addAction(UIAlertAction(title: name, style: .default) { _ in
            application.open(url, options: [:])
        })
    }
    alert.addAction(UIAlertAction(title: R.string.localizable.cancel(), style: .cancel, handler: nil))
    contextProvider.currentViewController.present(alert, animated: true, completion: nil)
}

Note this solution uses R.swift for string localization but you can replace those with NSLocalizedString normally, and it uses a contextProvider.currentViewController to get the presented UIViewController, but you can replace it with self if you are calling this in a view controller already.

As usual, you need to also add the following to your app Info.plist:

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>citymapper</string>
    <string>comgooglemaps</string>
    <string>waze</string>
</array>
Tomkin answered 28/4, 2020 at 11:19 Comment(0)
U
8

A SwiftUI approach based on @Angel G. Olloqui answer:

struct YourView: View {
    @State private var showingSheet = false

    var body: some View {
        VStack {
            Button(action: {
                showingSheet = true
            }) {
                Text("Navigate")
            }

        }
        .actionSheet(isPresented: $showingSheet) {
            let latitude = 45.5088
            let longitude = -73.554

            let appleURL = "http://maps.apple.com/?daddr=\(latitude),\(longitude)"
            let googleURL = "comgooglemaps://?daddr=\(latitude),\(longitude)&directionsmode=driving"
            let wazeURL = "waze://?ll=\(latitude),\(longitude)&navigate=false"

            let googleItem = ("Google Map", URL(string:googleURL)!)
            let wazeItem = ("Waze", URL(string:wazeURL)!)
            var installedNavigationApps = [("Apple Maps", URL(string:appleURL)!)]

            if UIApplication.shared.canOpenURL(googleItem.1) {
                installedNavigationApps.append(googleItem)
            }

            if UIApplication.shared.canOpenURL(wazeItem.1) {
                installedNavigationApps.append(wazeItem)
            }
            
            var buttons: [ActionSheet.Button] = []
            for app in installedNavigationApps {
                let button: ActionSheet.Button = .default(Text(app.0)) {
                    UIApplication.shared.open(app.1, options: [:], completionHandler: nil)
                }
                buttons.append(button)
            }
            let cancel: ActionSheet.Button = .cancel()
            buttons.append(cancel)
            
            return ActionSheet(title: Text("Navigate"), message: Text("Select an app..."), buttons: buttons)
        }
    }
}

Also, add the following to your Info.plist

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>googlechromes</string>
    <string>comgooglemaps</string>
    <string>waze</string>
</array>
Undulatory answered 25/1, 2021 at 8:2 Comment(0)
H
1

For anyone else looking for something similar you can now use UIActivityViewController, its the same UIControl Photos or Safari use when you click on the share button.

For apple maps and google maps you can add custom application activity to show alongside the other items. You need to subclass UIActivity and override the title and image methods. And the perform() function to handle the tap on our custom item

below is Objective C code i wrote for the same. For Swift code you can refer UIActivityViewController swift

    NSMutableArray *activityArray = [[NSMutableArray alloc] init];
// Check if google maps is installed and accordingly add it in menu
if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"comgooglemaps://"]]) {
  GoogleMapsActivityView *googleMapsActivity = [[GoogleMapsActivityView alloc] init];
  [activityArray addObject:googleMapsActivity];
}
// Check if apple maps is installed and accordingly add it in menu
if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"maps://"]]) {
  AppleMapsActivityView *appleMapsActivity = [[AppleMapsActivityView alloc] init];
  [activityArray addObject:appleMapsActivity];
}
NSArray *currentPlaces = [NSArray arrayWithObject:place];
UIActivityViewController *activityViewController =
    [[UIActivityViewController alloc] initWithActivityItems:currentPlaces
                                      applicationActivities:activityArray];
activityViewController.excludedActivityTypes = @[UIActivityTypePrint,
                                                 UIActivityTypeCopyToPasteboard,
                                                 UIActivityTypeAssignToContact,
                                                 UIActivityTypeSaveToCameraRoll,
                                                 UIActivityTypePostToWeibo,
                                                 UIActivityTypeAddToReadingList,
                                                 UIActivityTypePostToVimeo,
                                                 UIActivityTypeAirDrop];
[self presentViewController:activityViewController animated:YES completion:nil];

And Subclass the GoogleMapsActivity

@interface GoogleMapsActivityView: UIActivity

@end

@implementation GoogleMapsActivityView

- (NSString *)activityType {
  return @"yourApp.openplace.googlemaps";
}

- (NSString *)activityTitle {
  return NSLocalizedString(@"Open with Google Maps", @"Activity view title");
}

- (UIImage *)activityImage {
  return [UIImage imageNamed:@"ic_google_maps_logo"];
}

- (UIActivityCategory)activityCategory {
  return UIActivityCategoryAction;
}

- (BOOL)canPerformWithActivityItems:(NSArray *)activityItems {
  return YES;
}

- (void)performActivity {

  CLLocationDegrees lat = 99999;
  CLLocationDegrees lng = 99999;
  NSString *latlong = [NSString stringWithFormat:@"%.7f,%@%.7f", lat, @"", lng];
  NSString *urlString = [NSString stringWithFormat:@"comgooglemaps://?q=%@", latlong];

  if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:urlString]]) {
    [[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlString]
                                       options:@{}
                             completionHandler:nil];
  }
  [self activityDidFinish:YES];
}
Homograph answered 31/10, 2018 at 9:17 Comment(0)
W
1

SwiftUI rewrite from previous solutions using enums and a view modifier

extension View {
    func opensMap(at location: LocationCoordinate2D) -> some View {
        return self.modifier(OpenMapViewModifier(location: location))
    }
}

struct OpenMapViewModifier: ViewModifier {
    
    enum MapApp: CaseIterable {
        case apple, gmaps
        
        var title: String {
            switch self {
            case .apple: return "Apple Maps"
            case .gmaps: return "Google Maps"
            }
        }
        
        var scheme: String {
            switch self {
            case .apple: return "http"
            case .gmaps: return "comgooglemaps"
            }
        }
        
        var isInstalled: Bool {
            guard let url = URL(string: self.scheme.appending("://")) else { return false }
            return UIApplication.shared.canOpenURL(url)
        }
        
        func url(for location: LocationCoordinate2D) -> URL? {
            switch self {
            case .apple:
                return URL(string: "\(self.scheme)://maps.apple.com/?daddr=\(location.latitude),\(location.longitude)")
            case .gmaps:
                return URL(string: "\(self.scheme)://?daddr=\(location.latitude),\(location.longitude)&directionsmode=driving")
            }
        }
    }
    
    var location: LocationCoordinate2D
    
    @State private var showingAlert: Bool = false
    private let installedApps = MapApp.allCases.filter { $0.isInstalled }
    
    func body(content: Content) -> some View {
        Button(action: {
            if installedApps.count > 1 {
                showingAlert = true
            } else if let app = installedApps.first, let url = app.url(for: location) {
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
            }
        }) {
            content.actionSheet(isPresented: $showingAlert) {
                
                let appButtons: [ActionSheet.Button] = self.installedApps.compactMap { app in
                    guard let url = app.url(for: self.location) else { return nil }
                    return .default(Text(app.title)) {
                        UIApplication.shared.open(url, options: [:], completionHandler: nil)
                    }
                }
                return ActionSheet(title: Text("Navigate"), message: Text("Select an app..."), buttons: appButtons + [.cancel()])
            }
        }
    }
}
Winkelman answered 24/3, 2022 at 12:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.