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.
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:
Swift 5+
Base on #Emptyless answer.
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!
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>
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>
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];
}
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()])
}
}
}
}
Related
I'm implementing In-App purchases in an XCode project, and everything works fine except for one error. When the user isn't connected to the internet and he clicks a purchase button, the app crashes. I believe this happens because the in-app purchases haven't been fetched from iTunes and clicking the button can't run the purchase process. This crash also happens when the user clicks the button on the first second that the shop screen loads, because – I think – some time is needed to fetch (or request) the products. Here's what I'm talking about:
override func didMove(to view: SKView) {
...
fetchAvailableProducts()
}
func fetchAvailableProducts() {
// Put here your IAP Products ID's
let productIdentifiers = NSSet(objects:
productID100,
productID250,
productID500,
productIDRemoveAds,
productIDUnlockAll)
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers as! Set<String>)
productsRequest.delegate = self
productsRequest.start()
}
I'm basing my code on this tutorial.
Is there a way to change my code to make it "crash-proof", so that it first checks if the products can be bought to let you use the button?
I use SwiftyStorkeKit ( https://github.com/bizz84/SwiftyStoreKit ) , which uses a completion handler to fill in the products (will save you from reinventing the wheel as well - and is a great resource to learn from)
As for checking network connection, I use Apple's Reachability ( https://developer.apple.com/library/content/samplecode/Reachability/Introduction/Intro.html ). Here is the relevant parts of an implementation. It also check in situations where the app loses focus. You can also use the network check before any store operation.
class vcon: UIViewController {
#IBOutlet weak var noConnectionView: UIView!
func isNetworkAvailable() -> Bool {
//quick test if network is available
var netTest:Reachability? = Reachability(hostName: "apple.com")!
if netTest?.currentReachabilityStatus == .notReachable {
netTest = nil
return false
}
netTest = nil
return true
}
func displayNoNetworkView() {
//this example pulls from a storyboard to a view I have in front of everything else at all times, and shows the view to block everything else if the network isnt available
let ncvc = UIStoryboard(name: "HelpPrefsInfo", bundle: nil).instantiateViewController(withIdentifier: "noNetworkVC") as! noNetworkVC
ncvc.view.frame = noConnectionView.bounds
ncvc.view.backgroundColor = color03
ncvc.no_connection_imageView.tintColor = color01
ncvc.noInternetConnection_label.textColor = color01
noConnectionView.addSubview(ncvc.view)
}
func hideDataIfNoConnection() {
//the actual code that displays the no net connection view
if !isNetworkAvailable() {
if noConnectionView.isHidden == true {
noConnectionView.alpha = 0
noConnectionView.isHidden = false
self.iapObjects = []
UIView.animate(withDuration: 0.50, animations: {
self.noConnectionView.alpha = 1
}, completion:{(finished : Bool) in
});
}
} else {
if noConnectionView.isHidden == false {
self.collection_view.reloadData()
UIView.animate(withDuration: 0.50, animations: {
self.noConnectionView.alpha = 0
}, completion:{(finished : Bool) in
self.noConnectionView.isHidden = true
self.loadIAPData()
});
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: .UIApplicationDidBecomeActive, object: nil)
displayNoNetworkView()
loadIAPData()
}
func loadIAPData() {
load the data if the network is available
if isNetworkAvailable() {
helper.requestProductsWithCompletionHandler(completionHandler: { (success, products) -> Void in
if success {
self.iapObjects = products!
self.collection_view.reloadData()
} else {
let alert = UIAlertController(title: "Error", message: "Cannot retrieve products list right now.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
})
}
}
func willEnterForeground() {
hideDataIfNoConnection()
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
hideDataIfNoConnection()
}
In the case that a user may accidentally declines to receive notifications and wants to turn notifications later, how can I use an NSURL to open the IOS Settings App to my app's notification page where they can select Allow Notifications?
Updated 8 Dec, 2021:
This method will open Settings > Your App. It will show all available privacy toggles like camera, photos, notifications, cellular data, etc.
After a comment from #Mischa below, tested and updated the answer to this (more succinct):
if let appSettings = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(appSettings) {
UIApplication.shared.open(appSettings)
}
Previous answer:
I found the answer to this question (albeit helpful) has a bit too much assumed logic. Here is a plain and simple Swift 5 implementation if anyone else stumbles upon this question:
if let bundleIdentifier = Bundle.main.bundleIdentifier, let appSettings = URL(string: UIApplication.openSettingsURLString + bundleIdentifier) {
if UIApplication.shared.canOpenURL(appSettings) {
UIApplication.shared.open(appSettings)
}
}
For Swift 3, use UIApplicationOpenSettingsURLString to go to settings for your app where it shows the Notifications status and "Cellular Data"
let settingsButton = NSLocalizedString("Settings", comment: "")
let cancelButton = NSLocalizedString("Cancel", comment: "")
let message = NSLocalizedString("Your need to give a permission from notification settings.", comment: "")
let goToSettingsAlert = UIAlertController(title: "", message: message, preferredStyle: UIAlertControllerStyle.alert)
goToSettingsAlert.addAction(UIAlertAction(title: settingsButton, style: .destructive, handler: { (action: UIAlertAction) in
DispatchQueue.main.async {
guard let settingsUrl = URL(string: UIApplicationOpenSettingsURLString) else {
return
}
if UIApplication.shared.canOpenURL(settingsUrl) {
if #available(iOS 10.0, *) {
UIApplication.shared.open(settingsUrl, completionHandler: { (success) in
print("Settings opened: \(success)") // Prints true
})
} else {
UIApplication.shared.openURL(settingsUrl as URL)
}
}
}
}))
logoutUserAlert.addAction(UIAlertAction(title: cancelButton, style: .cancel, handler: nil))
Swift 5:
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
This should keep you covered for all iOS versions, including iOS 15 and iOS 16.
extension UIApplication {
private static let notificationSettingsURLString: String? = {
if #available(iOS 16, *) {
return UIApplication.openNotificationSettingsURLString
}
if #available(iOS 15.4, *) {
return UIApplicationOpenNotificationSettingsURLString
}
if #available(iOS 8.0, *) {
// just opens settings
return UIApplication.openSettingsURLString
}
// lol bruh
return nil
}()
private static let appNotificationSettingsURL = URL(
string: notificationSettingsURLString ?? ""
)
func openAppNotificationSettings() -> Bool {
guard
let url = UIApplication.appNotificationSettingsURL,
self.canOpen(url) else { return false }
return self.open(url)
}
}
Usage:
Button {
let opened = UIApplication.shared.openAppNotificationSettings()
if !opened {
print("lol fail")
}
} label: {
Text("Notifications")
}
UPDATE: This will be rejected by Apple.
To open notifications part of settings use this
UIApplication.shared.open(URL(string:"App-Prefs:root=NOTIFICATIONS_ID")!, options: [:], completionHandler: nil)
Since iOS 15.4 we can deeplink to the notif settings screen directly using UIApplicationOpenNotificationSettingsURLString:
https://developer.apple.com/documentation/uikit/uiapplicationopennotificationsettingsurlstring?language=objc
I have made a simple game using the Game template in Xcode, coded in swift. I created a shapeNode, and when it is touched, I would like this code to run:
if SLComposeViewController.isAvailableForServiceType(SLServiceTypeFacebook){
var controller = SLComposeViewController(forServiceType: SLServiceTypeFacebook)
controller.setInitialText("Testing Posting to Facebook")
//self.presentViewController(controller, animated:true, completion:nil)
}
This code is run in the GameViewController.swift file, but gives this error. This error occurs on the commented line.
Could not cast value of type 'UIView' (0x379480d0) to 'SKView' (0x37227ad0).
Update: If you are targeting iOS 9 or above there are some small changes to make this work. You will need to add the correct URL schemes to your info.plist otherwise the check to see if the app is installed will not work.
NOTE: Its is a better idea to now use UIActivityController for sharing. This allows you to only use 1 button and you can share to all sorts of services.
http://useyourloaf.com/blog/querying-url-schemes-with-canopenurl/
To present a viewController in a SKScene you need to use the rootViewController
self.view?.window?.rootViewController?.presentViewController(...
I use a little helper for this using swift 2 protocol extensions, so you can use it anywhere you like in your app. The Facebook part looks like this, twitter is basically the same.
import SpriteKit
import Social
/// URLString
private struct URLString {
static let iTunesApp = URL(string: "Your iTunes app link")
static let facebookApp = URL(string: "Your Facebook app link")
static let facebookWeb = URL(string: "Your Facebook web link")
}
/// Text strings
private struct TextString {
static let shareSheetText = "Your share sheet text"
static let error = "Error"
static let enableSocial = "Please sign in to your account first"
static let settings = "Settings"
static let ok = "OK"
}
/// Social
protocol Social {}
extension Social where Self: SKScene {
/// Open facebook
func openFacebook() {
guard let facebookApp = URLString.facebookApp else { return }
guard let facebookWeb = URLString.facebookWeb else { return }
if UIApplication.shared.canOpenURL(facebookApp){
UIApplication.shared.openURL(facebookApp)
} else {
UIApplication.shared.openURL(facebookWeb)
}
}
/// Share to facebook
func shareToFacebook() {
guard SLComposeViewController.isAvailable(forServiceType: SLServiceTypeFacebook) else {
showAlert()
return
}
guard let facebookSheet = SLComposeViewController(forServiceType: SLServiceTypeFacebook) else { return }
facebookSheet.completionHandler = { result in
switch result {
case .cancelled:
print("Facebook message cancelled")
break
case .done:
print("Facebook message complete")
break
}
}
let text = TextString.shareSheetText
//facebookSheet.setInitialText(text)
facebookSheet.setInitialText(String.localizedStringWithFormat(text, "add your score property")) // same as line above but with a score property
facebookSheet.addImage(Your UIImage)
facebookSheet.add(URLString.iTunesApp)
self.view?.window?.rootViewController?.present(facebookSheet, animated: true, completion: nil)
}
// MARK: - Private Methods
/// Show alert
private func showAlert() {
let alertController = UIAlertController(title: TextString.error, message: TextString.enableSocial, preferredStyle: .alert)
let okAction = UIAlertAction(title: TextString.ok, style: .cancel) { _ in }
alertController.addAction(okAction)
let settingsAction = UIAlertAction(title: TextString.settings, style: .default) { _ in
if let url = URL(string: UIApplicationOpenSettingsURLString) {
UIApplication.shared.openURL(url)
}
}
alertController.addAction(settingsAction)
self.view?.window?.rootViewController?.present(alertController, animated: true, completion: nil)
}
}
To use the helper you simply go to the SKScene you need to call the methods and implement the protocol
class YourScene: SKScene, Social {....
Now when the Facebook node/button is pressed you can call the methods as if they are part of the scene itself.
openFacebook() // opens app or safari
shareToFacebook() // opens share sheet textField
all thanks to swift 2 and protocol extensions. The cool bit about this is say you want to use this helper in a regular UIKit app, than all you have to do is import UIKit instead of spriteKit
import UIKit
and change the protocol extension to this
extension Social where Self: UIViewController {....
Its quite nice and very flexible I think
Hope this helps.
I am trying to implement a feature in an App that shows an alert when the internet connection is not available.
The alert has two actions (OK and Settings), whenever a user clicks on settings, I want to take them to the phone settings programmatically.
I am using Swift and Xcode.
Using UIApplication.openSettingsURLString
Update for Swift 5.1
override func viewDidAppear(_ animated: Bool) {
let alertController = UIAlertController (title: "Title", message: "Go to Settings?", preferredStyle: .alert)
let settingsAction = UIAlertAction(title: "Settings", style: .default) { (_) -> Void in
guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
return
}
if UIApplication.shared.canOpenURL(settingsUrl) {
UIApplication.shared.open(settingsUrl, completionHandler: { (success) in
print("Settings opened: \(success)") // Prints true
})
}
}
alertController.addAction(settingsAction)
let cancelAction = UIAlertAction(title: "Cancel", style: .default, handler: nil)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
}
Swift 4.2
override func viewDidAppear(_ animated: Bool) {
let alertController = UIAlertController (title: "Title", message: "Go to Settings?", preferredStyle: .alert)
let settingsAction = UIAlertAction(title: "Settings", style: .default) { (_) -> Void in
guard let settingsUrl = URL(string: UIApplicationOpenSettingsURLString) else {
return
}
if UIApplication.shared.canOpenURL(settingsUrl) {
UIApplication.shared.open(settingsUrl, completionHandler: { (success) in
print("Settings opened: \(success)") // Prints true
})
}
}
alertController.addAction(settingsAction)
let cancelAction = UIAlertAction(title: "Cancel", style: .default, handler: nil)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
}
⚠️ Be careful!
This answer is based on undocumented APIs and recently (since iOS12) Apple is rejecting apps with this approach.
Original answer below
Swift 5
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
Swift 4
UIApplication.shared.open(URL(string: UIApplicationOpenSettingsURLString)!, options: [:], completionHandler: nil)
NOTE: The following method works for all the versions below iOS 11, for higher versions the app might get rejected since it's a private API
Sometimes we want to take a user to settings other than our app settings.
The following method will help you achieve that:
First, configure the URL Schemes in your project. You will find it in Target -> Info -> URL Scheme. click on + button and type prefs in URL Schemes
Swift 5
UIApplication.shared.open(URL(string: "App-prefs:Bluetooth")!)
Swift 3
UIApplication.shared.open(URL(string:"App-Prefs:root=General")!, options: [:], completionHandler: nil)
Swift
UIApplication.sharedApplication().openURL(NSURL(string:"prefs:root=General")!)
Objective-C
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:#"prefs:root=General"]];
and following are all the available URLs
**On IOS < 12 **
prefs:root=General&path=About
prefs:root=General&path=ACCESSIBILITY
prefs:root=AIRPLANE_MODE
prefs:root=General&path=AUTOLOCK
prefs:root=General&path=USAGE/CELLULAR_USAGE
prefs:root=Brightness
prefs:root=Bluetooth
prefs:root=General&path=DATE_AND_TIME
prefs:root=FACETIME
prefs:root=General
prefs:root=General&path=Keyboard
prefs:root=CASTLE
prefs:root=CASTLE&path=STORAGE_AND_BACKUP
prefs:root=General&path=INTERNATIONAL
prefs:root=LOCATION_SERVICES
prefs:root=ACCOUNT_SETTINGS
prefs:root=MUSIC
prefs:root=MUSIC&path=EQ
prefs:root=MUSIC&path=VolumeLimit
prefs:root=General&path=Network
prefs:root=NIKE_PLUS_IPOD
prefs:root=NOTES
prefs:root=NOTIFICATIONS_ID
prefs:root=Phone
prefs:root=Photos
prefs:root=General&path=ManagedConfigurationList
prefs:root=General&path=Reset
prefs:root=Sounds&path=Ringtone
prefs:root=Safari
prefs:root=General&path=Assistant
prefs:root=Sounds
prefs:root=General&path=SOFTWARE_UPDATE_LINK
prefs:root=STORE
prefs:root=TWITTER
prefs:root=FACEBOOK
prefs:root=General&path=USAGE prefs:root=VIDEO
prefs:root=General&path=Network/VPN
prefs:root=Wallpaper
prefs:root=WIFI
prefs:root=INTERNET_TETHERING
prefs:root=Phone&path=Blocked
prefs:root=DO_NOT_DISTURB
On IOS 13
App-prefs:General&path=About
App-prefs:AIRPLANE_MODE
App-prefs:General&path=AUTOLOCK
App-prefs:Bluetooth
App-prefs:General&path=DATE_AND_TIME
App-prefs:FACETIME
App-prefs:General
App-prefs:General&path=Keyboard
App-prefs:CASTLE
App-prefs:CASTLE&path=STORAGE_AND_BACKUP
App-prefs:General&path=INTERNATIONAL
App-prefs:MUSIC
App-prefs:NOTES
App-prefs:NOTIFICATIONS_ID
App-prefs:Phone
App-prefs:Photos
App-prefs:General&path=ManagedConfigurationList
App-prefs:General&path=Reset
App-prefs:Sounds&path=Ringtone
App-prefs:Sounds
App-prefs:General&path=SOFTWARE_UPDATE_LINK
App-prefs:STORE
App-prefs:Wallpaper
App-prefs:WIFI
App-prefs:INTERNET_TETHERING
App-prefs:DO_NOT_DISTURB
Not tested
App-prefs:TWITTER (??)
App-prefs:FACEBOOK (??)
App-prefs:NIKE_PLUS_IPOD (??)
Note: Network setting will not be opened in a simulator, but the link will work on a real device.
SWIFT 5
if let settingsUrl = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(settingsUrl)
}
In iOS 8+ you can do the following:
func buttonClicked(sender:UIButton)
{
UIApplication.sharedApplication().openURL(NSURL(string: UIApplicationOpenSettingsURLString))
}
Swift 4
let settingsUrl = URL(string: UIApplicationOpenSettingsURLString)!
UIApplication.shared.open(settingsUrl)
Using #vivek's hint I develop an utils class based on Swift 3, hope you appreciate!
import Foundation
import UIKit
public enum PreferenceType: String {
case about = "General&path=About"
case accessibility = "General&path=ACCESSIBILITY"
case airplaneMode = "AIRPLANE_MODE"
case autolock = "General&path=AUTOLOCK"
case cellularUsage = "General&path=USAGE/CELLULAR_USAGE"
case brightness = "Brightness"
case bluetooth = "Bluetooth"
case dateAndTime = "General&path=DATE_AND_TIME"
case facetime = "FACETIME"
case general = "General"
case keyboard = "General&path=Keyboard"
case castle = "CASTLE"
case storageAndBackup = "CASTLE&path=STORAGE_AND_BACKUP"
case international = "General&path=INTERNATIONAL"
case locationServices = "LOCATION_SERVICES"
case accountSettings = "ACCOUNT_SETTINGS"
case music = "MUSIC"
case equalizer = "MUSIC&path=EQ"
case volumeLimit = "MUSIC&path=VolumeLimit"
case network = "General&path=Network"
case nikePlusIPod = "NIKE_PLUS_IPOD"
case notes = "NOTES"
case notificationsId = "NOTIFICATIONS_ID"
case phone = "Phone"
case photos = "Photos"
case managedConfigurationList = "General&path=ManagedConfigurationList"
case reset = "General&path=Reset"
case ringtone = "Sounds&path=Ringtone"
case safari = "Safari"
case assistant = "General&path=Assistant"
case sounds = "Sounds"
case softwareUpdateLink = "General&path=SOFTWARE_UPDATE_LINK"
case store = "STORE"
case twitter = "TWITTER"
case facebook = "FACEBOOK"
case usage = "General&path=USAGE"
case video = "VIDEO"
case vpn = "General&path=Network/VPN"
case wallpaper = "Wallpaper"
case wifi = "WIFI"
case tethering = "INTERNET_TETHERING"
case blocked = "Phone&path=Blocked"
case doNotDisturb = "DO_NOT_DISTURB"
}
enum PreferenceExplorerError: Error {
case notFound(String)
}
open class PreferencesExplorer {
// MARK: - Class properties -
static private let preferencePath = "App-Prefs:root"
// MARK: - Class methods -
static func open(_ preferenceType: PreferenceType) throws {
let appPath = "\(PreferencesExplorer.preferencePath)=\(preferenceType.rawValue)"
if let url = URL(string: appPath) {
if #available(iOS 10.0, *) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else {
UIApplication.shared.openURL(url)
}
} else {
throw PreferenceExplorerError.notFound(appPath)
}
}
}
This is very helpful since that API's will change for sure and you can refactor once and very fast!
The first response from App-Specific URL Schemes worked for me on iOS 10.3.
if let appSettings = URL(string: UIApplicationOpenSettingsURLString + Bundle.main.bundleIdentifier!) {
if UIApplication.shared.canOpenURL(appSettings) {
UIApplication.shared.open(appSettings)
}
}
App-Prefs:root=Privacy&path=LOCATION worked for me for getting to general location settings. Note: only works on a device.
iOS 12+
The open(url:options:completionHandler:) method has been updated to include a non-nil options dictionary, which as of this post only contains one possible option of type UIApplication.OpenExternalURLOptionsKey (in the example).
#objc func openAppSpecificSettings() {
guard let url = URL(string: UIApplication.openSettingsURLString),
UIApplication.shared.canOpenURL(url) else {
return
}
let optionsKeyDictionary = [UIApplication.OpenExternalURLOptionsKey(rawValue: "universalLinksOnly"): NSNumber(value: true)]
UIApplication.shared.open(url, options: optionsKeyDictionary, completionHandler: nil)
}
Explicitly constructing a URL, such as with "App-Prefs", has gotten some apps rejected from the store.
word of warning: the prefs:root or App-Prefs:root URL schemes are considered private API. Apple may reject you app if you use those, here is what you may get when submitting your app:
Your app uses the "prefs:root=" non-public URL scheme, which is a private entity. The use of non-public APIs is not permitted on the App Store because it can lead to a poor user experience should these APIs change. Continuing to use or conceal non-public APIs in future submissions of this app may result in the termination of your Apple Developer account, as well as removal of all associated apps from the App Store.
Next Steps
To resolve this issue, please revise your app to provide the associated functionality using public APIs or remove the functionality using the "prefs:root" or "App-Prefs:root" URL scheme.
If there are no alternatives for providing the functionality your app requires, you can file an enhancement request.
in ios10/ Xcode 8 in simulator:
UIApplication.shared.openURL(URL(string:UIApplicationOpenSettingsURLString)!)
works
UIApplication.shared.openURL(URL(string:"prefs:root=General")!)
does not.
I have seen this line of code
UIApplication.sharedApplication() .openURL(NSURL(string:"prefs:root=General")!)
is not working, it didn't work for me in ios10/ Xcode 8, just a small code difference, please replace this with
UIApplication.sharedApplication().openURL(NSURL(string:"App-Prefs:root=General")!)
Swift3
UIApplication.shared.openURL(URL(string:"prefs:root=General")!)
Replace with
UIApplication.shared.openURL(URL(string:"App-Prefs:root=General")!)
Hope it helps.
Cheers.
Adding to #Luca Davanzo
iOS 11, some permissions settings have moved to the app path:
iOS 11 Support
static func open(_ preferenceType: PreferenceType) throws {
var preferencePath: String
if #available(iOS 11.0, *), preferenceType == .video || preferenceType == .locationServices || preferenceType == .photos {
preferencePath = UIApplicationOpenSettingsURLString
} else {
preferencePath = "\(PreferencesExplorer.preferencePath)=\(preferenceType.rawValue)"
}
if let url = URL(string: preferencePath) {
if #available(iOS 10.0, *) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else {
UIApplication.shared.openURL(url)
}
} else {
throw PreferenceExplorerError.notFound(preferencePath)
}
}
SWIFT 4
This could take your app's specific settings, if that's what you're looking for.
UIApplication.shared.openURL(URL(string: UIApplicationOpenSettingsURLString)!)
As above #niravdesai said App-prefs.
I found that App-Prefs: works for both iOS 9, 10 and 11. devices tested.
where as prefs: only works on iOS 9.
UIApplication.open(_:options:completionHandler:) must be used from main thread only
Solution:
if let appSettings = URL(string: UIApplication.openSettingsURLString + Bundle.main.bundleIdentifier!) {
if UIApplication.shared.canOpenURL(appSettings) {
DispatchQueue.main.async {
UIApplication.shared.open(appSettings)
}
}
}
I started to work with Google Cast API in iOS and I'm trying to get it to work with Apple new programming language Swift. I used as a reference Google Sample apps at https://github.com/googlecast/CastVideos-ios. The problem is when it connect to a cast device and it gets here..
func deviceManager(deviceManager: GCKDeviceManager!, didConnectToCastApplication applicationMetadata: GCKApplicationMetadata!, sessionID: String!, launchedApplication: Bool) {
NSLog("application has launched \(launchedApplication)")
mediaControlChannel = GCKMediaControlChannel.alloc()
mediaControlChannel.delegate = self
self.deviceManager.addChannel(mediaControlChannel)
mediaControlChannel.requestStatus()
NSLog("waarde van requestStatus is: \(mediaControlChannel.requestStatus())")
}
It launches and the NSLog is giving me this "application has launched : false" and then I get the following error.. "Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'setObjectForKey: key cannot be nil'" I have looked in the Google Cast iOS class documentation and I found this https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_media_metadata
Then I found this in that class
- (id) objectForKey: (NSString *) key
Reads the value of a field.
So my thought was that I have to put something here..
#IBAction func sendVideo(sender: AnyObject) {
NSLog("Cast video")
if deviceManager == nil {
var alert = UIAlertController(title: "Not connected", message: "Please connect to Cast device", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
presentViewController(alert, animated: true, completion: nil)
return
}
// Define media metadata
var metaData = GCKMediaMetadata(metadataType: GCKMediaMetadataType.User)
metaData.setString("Bug Bunny!", forKey: kGCKMetadataKeyTitle)
metaData.setString("dit is allemaal maar was subtitles dasdsadhjsdfgjkkHDHSAGDH", forKey: kGCKMetadataKeySubtitle)
var url = NSURL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg")
metaData.addImage(GCKImage(URL: url, width: 480, height: 360))
var mediaInformation = GCKMediaInformation(contentID: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg", streamType: .None, contentType: "video/mp4", metadata: metaData as GCKMediaMetadata, streamDuration: 0, customData: nil)
NSLog("waarde van mediainformation is : \(mediaInformation)")
mediaControlChannel.loadMedia(mediaInformation, autoplay: true, playPosition: 0)
}
As Jesper pointed out, the GCKMediaControlChannel.alloc() line is wrong.
Here's a Swift ported didConnectToCastApplication method (from the Google CastVideos-ios example) that I know works:
func deviceManager(deviceManager: GCKDeviceManager!, didConnectToCastApplication applicationMetadata: GCKApplicationMetadata!, sessionID: String!, launchedApplication: Bool) {
self.isReconnecting = false
self.mediaControlChannel = GCKMediaControlChannel()
self.mediaControlChannel?.delegate = self
self.receiverCommandChannel = ChromecastCommandChannel()
self.receiverCommandChannel?.delegate = self
self.deviceManager?.addChannel(self.mediaControlChannel)
self.deviceManager?.addChannel(self.receiverCommandChannel)
self.mediaControlChannel?.requestStatus()
self.receiverCommandChannel?.requestStatus()
self.applicationMetadata = applicationMetadata
self.updateCastIconButtonStates()
if let delegate = self.delegate {
if let device = self.selectedDevice {
delegate.didConnectToDevice(device)
} else {
NSLog("Can not connect to device as no selected device is set")
}
} else {
NSLog("Can not run didConnectToDevice as delegate is not set")
}
// Store sessionId in case of restart
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setObject(sessionID, forKey: "lastCCSessionId")
if let deviceId = self.selectedDevice?.deviceID {
defaults.setObject(deviceId, forKey: "lastCCDeviceId")
}
defaults.synchronize()
}