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()
}
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
In my app i have a option to login to the app using google sign in. Login is working fine. Once i click the Logout Button, I am not able to logout from google. When ever I click on login button it's not showing login page as image given below:
Instead its redirecting to authentication dialog page as image given below:
Code :
override func viewDidLoad()
{
super.viewDidLoad()
GIDSignIn.sharedInstance().uiDelegate = self
let button = GIDSignInButton(frame: CGRectMake(0, 0, 100, 100))
button.center = view.center
view.addSubview(button)
}
#IBAction func signOutButton(sender: AnyObject) {
GIDSignIn.sharedInstance().signOut()
}
1.import below
import GoogleAPIClient
import GTMOAuth2
2.declare below variable
let kKeychainItemName = "your app name"
let kClientID = "your app clinet id"
let scopes = [kGTLAuthScopeDrive]
let service = GTLServiceDrive()
3.replace this methods to your existing one
override func viewDidLoad() {
super.viewDidLoad()
if let auth = GTMOAuth2ViewControllerTouch.authForGoogleFromKeychainForName(
kKeychainItemName,
clientID: kClientID,
clientSecret: nil) {
service.authorizer = auth
}
self.tblView.tableFooterView=UIView()
// Do any additional setup after loading the view.
}
override func viewDidAppear(animated: Bool) {
if let authorizer = service.authorizer,
canAuth = authorizer.canAuthorize where canAuth {
fetchFiles()
} else {
presentViewController(
createAuthController(),
animated: true,
completion: nil
)
}
}
func fetchFiles() {
let query = GTLQueryDrive.queryForFilesList()
query.pageSize = 10
query.fields = "nextPageToken, files(id, name)"
service.executeQuery(
query,
delegate: self,
didFinishSelector: #selector(GoogleDriveVC.displayResultWithTicket(_:finishedWithObject:error:))
)
}
// Parse results and display
func displayResultWithTicket(ticket : GTLServiceTicket,
finishedWithObject response : GTLDriveFileList,
error : NSError?) {
if let error = error {
showAlert("Error", message: error.localizedDescription)
return
}
if let files = response.files where !files.isEmpty {
for file in files as! [GTLDriveFile] {
self.arrayOfNames.append(file.name)
self.arrayOfIdentifier.append(file.identifier)
}
}
self.tblView.reloadData()
}
// Creates the auth controller for authorizing access to Drive API
private func createAuthController() -> GTMOAuth2ViewControllerTouch {
let scopeString = scopes.joinWithSeparator(" ")
return GTMOAuth2ViewControllerTouch(
scope: scopeString,
clientID: kClientID,
clientSecret: nil,
keychainItemName: kKeychainItemName,
delegate: self,
finishedSelector: #selector(GoogleDriveVC.viewController(_:finishedWithAuth:error:))
)
}
// Handle completion of the authorization process, and update the Drive API
// with the new credentials.
func viewController(vc : UIViewController,
finishedWithAuth authResult : GTMOAuth2Authentication, error : NSError?) {
if let error = error {
service.authorizer = nil
showAlert("Authentication Error", message: error.localizedDescription)
return
}
service.authorizer = authResult
dismissViewControllerAnimated(true, completion: nil)
}
// Helper for showing an alert
func showAlert(title : String, message: String) {
let alert = UIAlertController(
title: title,
message: message,
preferredStyle: UIAlertControllerStyle.Alert
)
let ok = UIAlertAction(
title: "OK",
style: UIAlertActionStyle.Default,
handler: nil
)
alert.addAction(ok)
presentViewController(alert, animated: true, completion: nil)
}
at last logout using
func logout(){
//logout code
GTMOAuth2ViewControllerTouch.removeAuthFromKeychainForName(kKeychainItemName)
navigationController?.popViewControllerAnimated(true)
}
this is the full implementation
If OP is still looking (doubt it since it's been 3 years) and if anyone is still working on this I think I figured it out.
I'm using Objective C but the methodology is still the same.
I have an app that uses Google Sign In to authenticate. This work as OP has described. For our sign out I have the following:
(IBAction)didTapSignOut:(id)sender
{
GIDSignIn *gidObject = [GIDSignIn sharedInstance];
[gidObject signOut];
[gidObject disconnect];
NSString *logOutUrl = #"https://www.google.com/accounts/Logout";
[[UIApplication sharedApplication] openURL:[NSURL URLWithString: logOutUrl] options:#{} completionHandler:nil];
}
Make sure signOut is before disconnect (i originally had those two reversed and it did not sign the user out).
For our workflow I have it so that the logout url appears. This works because if the user wants to sign in again then they need to authenticate. If the user is signed in already it bypasses the authentication and goes directly into application.
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()])
}
}
}
}
I want to use swift to implement in-app email. When I click the button, the email window pops up. However, I am unable to send my email. Moreover, after I click cancel-delete draft, I cannot go back to the original screen.
import UIkit
import MessageUI
class Information : UIViewController, MFMailComposeViewControllerDelegate{
var myMail: MFMailComposeViewController!
#IBAction func sendReport(sender : AnyObject) {
if(MFMailComposeViewController.canSendMail()){
myMail = MFMailComposeViewController()
//myMail.mailComposeDelegate
// set the subject
myMail.setSubject("My report")
//To recipients
var toRecipients = ["lipeilin#gatech.edu"]
myMail.setToRecipients(toRecipients)
//CC recipients
var ccRecipients = ["tzhang85#gatech.edu"]
myMail.setCcRecipients(ccRecipients)
//CC recipients
var bccRecipients = ["tzhang85#gatech.edu"]
myMail.setBccRecipients(ccRecipients)
//Add some text to the message body
var sentfrom = "Email sent from my app"
myMail.setMessageBody(sentfrom, isHTML: true)
//Include an attachment
var image = UIImage(named: "Gimme.png")
var imageData = UIImageJPEGRepresentation(image, 1.0)
myMail.addAttachmentData(imageData, mimeType: "image/jped", fileName: "image")
//Display the view controller
self.presentViewController(myMail, animated: true, completion: nil)
}
else{
var alert = UIAlertController(title: "Alert", message: "Your device cannot send emails", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
}
func mailComposeController(controller: MFMailComposeViewController!,
didFinishWithResult result: MFMailComposeResult,
error: NSError!){
switch(result.value){
case MFMailComposeResultSent.value:
println("Email sent")
default:
println("Whoops")
}
self.dismissViewControllerAnimated(true, completion: nil)
}
}
Since you haven't set the current view controller as the mailComposeDelegate of myMail, the mailComposeController:didFinishWithResult method isn't being called. After you init myMail, make sure to add:
myMail.mailComposeDelegate = self
and you'll be good to go
In case anyone is looking for a non MFMailCompose option, here's what I did to send using Gmail's SMTP servers.
Download a zip of this repo: https://github.com/jetseven/skpsmtpmessage
Drag and drop the files under SMTPLibrary into your XCode project
Create a new header file - MyApp-Briding-Header.h
Replace new header file with this:
#import "Base64Transcoder.h"
#import "HSK_CFUtilities.h"
#import "NSData+Base64Additions.h"
#import "NSStream+SKPSMTPExtensions.h"
#import "SKPSMTPMessage.h"
Go to Project(Targets > MyApp on the left)/Build Settings/Swift Compiler - Code Generation
Add path to header file under Objective-C Briding Header -> Debug (i.e. MyApp/MyApp-Bridging-Header.h
Go to Project/Build Phases/Compile Sources
Select all .m files and click enter. Type -fno-objc-arc and hit enter.
Use this code to send email:
var mail = SKPSMTPMessage()
mail.fromEmail = "fromemail#gmail.com"
mail.toEmail = "tomail#gmail.com"
mail.requiresAuth = true
mail.login = "fromemail#gmail.com"
mail.pass = "password"
mail.subject = "test subject"
mail.wantsSecure = true
mail.relayHost = "smtp.gmail.com"
mail.relayPorts = [587]
var parts: NSDictionary = [
"kSKPSMTPPartContentTypeKey": "text/plain; charset=UTF-8",
"kSKPSMTPPartMessageKey": "test message",
]
mail.parts = [parts]
mail.send()
Hope it helps someone. I didn't want to use the MFMailCompose option because I didn't want to have to prompt the user.
This is how I have composed my email with attached PDF file document.
Just to test this example you need to drag and drop a sample PDF named "All_about_tax.pdf"
#IBAction func sendEmail(sender: UIButton)
{
//Check to see the device can send email.
if( MFMailComposeViewController.canSendMail() )
{
print("Can send email.")
let mailComposer = MFMailComposeViewController()
mailComposer.mailComposeDelegate = self
//Set to recipients
mailComposer.setToRecipients(["your email id here"])
//Set the subject
mailComposer.setSubject("Tax info document pdf")
//set mail body
mailComposer.setMessageBody("This is what they sound like.", isHTML: true)
if let filePath = NSBundle.mainBundle().pathForResource("All_about_tax", ofType: "pdf")
{
print("File path loaded.")
if let fileData = NSData(contentsOfFile: filePath)
{
print("File data loaded.")
mailComposer.addAttachmentData(fileData, mimeType: "application/pdf", fileName: "All_about_tax.pdf")
}
}
//this will compose and present mail to user
self.presentViewController(mailComposer, animated: true, completion: nil)
}
else
{
print("email is not supported")
}
}
func mailComposeController(controller: MFMailComposeViewController, didFinishWithResult result: MFMailComposeResult, error: NSError?)
{
self.dismissViewControllerAnimated(true, completion: nil)
}