I have a Safari share extension where I want the ability to open the main app from within the extension. The user is presented with an alert where they have the option to open the app.
func openAppHandler() {
self.extensionContext?.completeRequest(returningItems: []) { (success) in
if let url = URL(string: "myapp://...") {
self.extensionContext?.open(url, completionHandler: nil)
}
}
}
The alert appears after the method didSelectPost() is called, and as you can see it occurs in the background priority completion block for the extension. The open method says in it's docs "In iOS 8, only the Today extension point (used for creating widgets) supports this method." I'm guessing it's still the case that it's still not supported in the Safari Share Extension.
Does anyone know of a way to open my main app from a share extension?
I found a solution here. I'm not sure if this is technically ok with Apple, but it works just as I need it to.
#objc func openURL(_ url: URL) {
return
}
func openContainerApp() {
var responder: UIResponder? = self as UIResponder
let selector = #selector(MyViewController.openURL(_:))
while responder != nil {
if responder!.responds(to: selector) && responder != self {
responder!.perform(selector, with: URL(string: "myapp://url")!)
return
}
responder = responder?.next
}
}
Related
I am just using phone dialer in my swift code it is working fine but I don't know why the screen is getting refreshed when I am tapping on cancel button.
if let phoneCallURL = URL(string: "telprompt://1234") {
let application:UIApplication = UIApplication.shared
if (application.canOpenURL(phoneCallURL)) {
if #available(iOS 10.0, *) {
application.open(phoneCallURL, options: [:], completionHandler: nil)
} else {
// Fallback on earlier versions
application.openURL(phoneCallURL as URL)
}
}
}
When the phone call action sheet appears, the OS is hanling the interaction, so your app will become inactive, and the applicationWillResignActive delegate function will be called.
Once cancel button is taped, your app become active, and you may find some loading code in your applicationDidBecomeActive delegate function. You can handle the details in this function.
check offical document to find more details of app life cycle.
I'm adding an iMessage extension target to my app. The extension is supposed to send a message that has a url attribute. The behaviour I'm expecting when a user touches the message is to open the browser using the url attribute of the message.
I have a button in my messageView which executes this code:
#IBAction func labelButton(_ sender: Any) {
let layout = MSMessageTemplateLayout()
layout.imageTitle = "iMessage Extension"
layout.caption = "Hello world!"
layout.subcaption = "Test sub"
guard let url: URL = URL(string: "https://google.com") else { return }
let message = MSMessage()
message.layout = layout
message.summaryText = "Sent Hello World message"
message.url = url
activeConversation?.insert(message, completionHandler: nil)
}
If I touch the message, it expands the MessageViewController
I have then added this:
override func didSelect(_ message: MSMessage, conversation: MSConversation) {
if let message = conversation.selectedMessage {
// message selected
// Eg. open your app:
self.extensionContext?.open(message.url!, completionHandler: nil)
}
}
And now, when I touch the message, it opens my main app but still not my browser.
I have seen on another post (where I cannot comment, thus I opened this post) that it is impossible to open in Safari but I have a news app which inserts links to articles and allows with a click on the message to open the article in a browser window, while the app is installed.
So, can someone please tell how I can proceed to force opening the link in a browser window?
Thank you very much.
Here is a trick to insert a link in a message. It does not allow to create an object that has an url attribute but just to insert a link directly which will open in the default web browser.
activeConversation?.insertText("https://google.com", completionHandler: nil)
I have published a sample on github showing how to launch a URL from inside an iMessage extension. It just uses a fixed URL but the launching code is what you need.
Copying from my readme
The obvious thing to try is self.extensionContext.open which is documented as Asks the system to open a URL on behalf of the currently running app extension.
That doesn't work. However, you can iterate back up the responder chain to find a suitable handler for the open method (actually the iMessage instance) and invoke open with that object.
This approach works for URLs which will open a local app, like settings for a camera, or for web URLs.
The main code
#IBAction public func onOpenWeb(_ sender: UIButton) {
guard let url = testUrl else {return}
// technique that works rather than self.extensionContext.open
var responder = self as UIResponder?
let handler = { (success:Bool) -> () in
if success {
os_log("Finished opening URL")
} else {
os_log("Failed to open URL")
}
}
let openSel = #selector(UIApplication.open(_:options:completionHandler:))
while (responder != nil){
if responder?.responds(to: openSel ) == true{
// cannot package up multiple args to openSel so we explicitly call it on the iMessage application instance
// found by iterating up the chain
(responder as? UIApplication)?.open(url, completionHandler:handler) // perform(openSel, with: url)
return
}
responder = responder!.next
}
}
I want to manage some action in containing app from today extension(Widget).
Full description:
in my containing app, some action (like play/pause audio) perform. And want to manage that action also from today extension(widget). An action continues to perform in background state as well.
In today extension, the same action will perform. so for that, if in the main containing app already starts an action and send it into background state, a user can pause action from a widget. and the user also can start/pause action any time from the widget (today extension).
For achieve this goal I used UserDefault with app Group capability and store one boolean value. when widget present it checks boolean value and set button state play/pause. it's set correctly but when I press extension button action does not perform in host app.
code:
in main containing app code
override func viewDidLoad() {
super.viewDidLoad()
let objUserDefault = UserDefaults(suiteName:"group.test.TodayExtensionSharingDefaults")
let objTemp = objUserDefault?.object(forKey: "value")
self.btnValue.isSelected = objTemp
NotificationCenter.default.addObserver(self, selector: #selector(self.userDefaultsDidChange), name: UserDefaults.didChangeNotification, object: nil)
}
func userDefaultsDidChange(_ notification: Notification) {
let objUserDefault = UserDefaults(suiteName: "group.test.TodayExtensionSharingDefaults")
objUserDefault?.synchronize()
let objTemp = objUserDefault?.object(forKey: "value")
self.btnValue.isSelected = objTemp
}
In Extension Class:
#IBAction func onPlayPause(_ sender: UIButton) {
DispatchQueue.main.async {
let sharedDefaults = UserDefaults(suiteName: "group.test.TodayExtensionSharingDefaults")
if let isPlaying = sharedDefaults?.bool(forKey: "isPlaing") {
sharedDefaults?.set(!isPlaying, forKey: "isPlaying")
}else{
sharedDefaults?.set(false, forKey: "isPlaying")
}
sharedDefaults?.synchronize()
}
notification was not fired when a user updates default. it's updated value when the app restarts.
so how to solve this issue?
and the same thing wants to do in opposite means from containing app to a widget.
(easy to user single action object but how?)
And is any other way to perform a quick action in containing app from extension without opening App?
Use MMWormhole (or its new and unofficial Swift version, just Wormhole). It's very simple.
In the app's view controller:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let wormhole = MMWormhole(applicationGroupIdentifier: "group.test.TodayExtensionSharingDefaults",
optionalDirectory: "TodayExtensionSharingDefaults")
wormhole.listenForMessage(withIdentifier: "togglePlayPause") { [weak self] _ in
guard let controller = self else { return }
controller.btnValue.isSelected = controller.btnValue.isSelected
}
}
In the extension:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view from its nib.
self.wormhole = MMWormhole(applicationGroupIdentifier: "group.test.TodayExtensionSharingDefaults", optionalDirectory: "TodayExtensionSharingDefaults")
}
#IBAction func onPlayPause(_ sender: UIButton) {
guard let wormhole = self.wormhole else { extensionContext?.openURL(NSURL(string: "foo://startPlaying")!, completionHandler: nil) } // Throw error here instead of return, since somehow this function was called before viewDidLoad (or something else went horribly wrong)
wormhole.passMessageObject(nil, identifier: "togglePlayPause")
}
Declare foo:// (or whatever else you use) in Xcode's Document Types section, under URLs, then implement application(_:open:options:) in your AppDelegate so that the app starts playing music when the URL passed is foo://startPlaying.
Create Custom URL Sceheme
Check groups data.(are you setting correct or not)
Whenever you click on button, the host app will get called from Appdelegate, UIApplication delegate
func application(_ application: UIApplication, open urls: URL, sourceApplication: String?, annotation: Any) -> Bool {
let obj = urls.absoluteString.components(separatedBy: "://")[1]
NotificationCenter.default.post(name: widgetNotificationName, object: obj)
print("App delegate")
return true
}
Fire your notification from there then observe it anywhere in your hostapp.
Widget Button action code
#IBAction func doActionMethod(_ sender: AnyObject) {
let button = (sender as! UIButton)
var dailyThanthi = ""
switch button.tag {
case 0:
dailyThanthi = "DailyThanthi://h"
case 1:
dailyThanthi = "DailyThanthi://c"
case 2:
dailyThanthi = "DailyThanthi://j"
// case 3:
// dailyThanthi = "DailyThanthi://s"
// case 4:
// dailyThanthi = "DailyThanthi://s"
default:
break
}
let pjURL = NSURL(string: dailyThanthi)!
self.extensionContext!.open(pjURL as URL, completionHandler: nil)
}
Check out custom url type:
https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/Inter-AppCommunication/Inter-AppCommunication.html
Note:
There is no direct communication between an app extension and its
containing app; typically, the containing app isn’t even running while
a contained extension is running. An app extension’s containing app
and the host app don’t communicate at all.
In a typical request/response transaction, the system opens an app extension on behalf of a host app, conveying data in an extension
context provided by the host. The extension displays a user interface,
performs some work, and, if appropriate for the extension’s purpose,
returns data to the host.
The dotted line in Figure 2-2 represents the limited interaction available between an app extension and its containing app. A Today
widget (and no other app extension type) can ask the system to open
its containing app by calling the openURL:completionHandler: method of
the NSExtensionContext class. As indicated by the Read/Write arrows in
Figure 2-3, any app extension and its containing app can access shared
data in a privately defined shared container. The full vocabulary of
communication between an extension, its host app, and its containing
app is shown in simple form in Figure 2-3.
https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/ExtensionOverview.html
I am creating an iOS Application iMessage Extension.
According to Example by Apple, I creating a message according to provided logic
guard let url: URL = URL(string: "http://www.google.com") else { return }
let message = composeMessage(url: url)
activeConversation?.insert(message, completionHandler: { [weak self] (error: Error?) in
guard let error = error else { return }
self?.presentAlert(error: error)
})
also
private func composeMessage(url: URL) -> MSMessage {
let layout = MSMessageTemplateLayout()
layout.caption = "caption"
layout.subcaption = "subcaption"
layout.trailingSubcaption = "trailing subcaption"
let message = MSMessage()
message.url = url
message.layout = layout
return message
}
and
private func presentAlert(error: Error) {
let alertController: UIAlertController = UIAlertController(
title: "Error",
message: error.localizedDescription,
preferredStyle: .alert
)
let cancelAction: UIAlertAction = UIAlertAction(
title: "OK",
style: .cancel,
handler: nil
)
alertController.addAction(cancelAction)
present(
alertController,
animated: true,
completion: nil
)
}
As far as I understand, after message is sent, on a click, Safari browser should be opened.
When I click on a sent message, MessageViewController screen takes place in whole screen, without opening safari or another app.
Where is the problem? How can I achieve desired functionality?
Here is the code I use to open a URL from a iMessage extension. It is currently working to open the Music app in the WATUU iMessage application. For instance with the URL
"https://itunes.apple.com/us/album/as%C3%AD/1154300311?i=1154300401&uo=4&app=music"
This functionality currently works in iOS 10, 11 and 12
func openInMessagingURL(urlString: String){
if let url = NSURL(string:urlString){
let context = NSExtensionContext()
context.open(url, completionHandler: nil)
var responder = self as UIResponder?
while (responder != nil){
if responder?.responds(to: Selector("openURL:")) == true{
responder?.perform(Selector("openURL:"), with: url)
}
responder = responder!.next
}
}
}
UPDATE FOR SWIFT 4
func openInMessagingURL(urlString: String){
if let url = URL(string:urlString){
let context = NSExtensionContext()
context.open(url, completionHandler: nil)
var responder = self as UIResponder?
while (responder != nil){
if responder?.responds(to: #selector(UIApplication.open(_:options:completionHandler:))) == true{
responder?.perform(#selector(UIApplication.open(_:options:completionHandler:)), with: url)
}
responder = responder!.next
}
}
}
I think safari Browser only opens for macOS. This worked for me:
override func didSelectMessage(message: MSMessage, conversation: MSConversation) {
if let message = conversation.selectedMessage {
// message selected
// Eg. open your app:
let url = // your apps url
self.extensionContext?.openURL(url, completionHandler: { (success: Bool) in
})
}
}
Using the technique shown by Julio Bailon
Fixed for Swift 4 and that openURL has been deprecated.
Note that the extensionContext?.openURL technique does not work from an iMessage extension - it only opens your current app.
I have posted a full sample app showing the technique on GitHub with the relevant snippet here:
let handler = { (success:Bool) -> () in
if success {
os_log("Finished opening URL")
} else {
os_log("Failed to open URL")
}
}
let openSel = #selector(UIApplication.open(_:options:completionHandler:))
while (responder != nil){
if responder?.responds(to: openSel ) == true{
// cannot package up multiple args to openSel so we explicitly call it on the iMessage application instance
// found by iterating up the chain
(responder as? UIApplication)?.open(url, completionHandler:handler) // perform(openSel, with: url)
return
}
responder = responder!.next
}
It seems it is not possible to open an app from a Message Extension, except the companion app contained in the Workspace. We have tried to open Safari from our Message Extension, it did not work, this limitation seems by design.
You could try other scenari to solve your problem :
Webview in Expanded Message Extension
You could have a Webview in your Message Extension, and when you click
on a message, you could open the Expanded mode and open you Url in the
Webview.
The user won't be in Safari, but the page will be embedded in your Message Extension.
Open the Url in the Companion App
On a click on the message, you could open your Companion app (through
the Url Scheme with MyApp://?myParam=myValue) with a special parameter
; the Companion app should react to this parameter and could redirect
to Safari through OpenUrl.
In this case, you'll several redirects before the WebPage, but it should allow to go back to the conversation.
We have also found that we could instance a SKStoreProductViewController in a Message Extension, if you want to open the Apple Store right in Messages and let the user buy items.
If you only need to insert a link, then you should use activeConversation.insertText and insert the link. Touching the message will open Safari.
openURL in didSelectMessage:conversation: by using extensionContext
handle the URL scheme in your host AppDelegate
I have an iOS universal link targeted at myApp. When I click that link in another app, myApp opens and displays the right payoff perfectly, it's working.
But myApp includes a built-in browser using WKWebView. When I click the same universal link from within my built-in browser, iOS doesn't send the link to myApp, it goes and fetches a webpage.
Apple docs say
If you instantiate a SFSafariViewController, WKWebView, or UIWebView object to handle a universal link, iOS opens your website in Safari instead of opening your app. However, if the user taps a universal link within an embedded SFSafariViewController, WKWebView, or UIWebView object, iOS opens your app.
I note this similar question where the suggestion was to define a WKWebView delegate. I have both WKWebView delegates defined and in use in myApp, and it's not helping. This other question has lots of upvotes but no answers.
My WKWebView can actually open universal links to other apps. If I click the link https://open.spotify.com/artist/3hv9jJF3adDNsBSIQDqcjp from within the myApp WKWebView then it opens the spotify app without opening any intermediate webpage (even though the long-press menu in myApp doesn't offer to "open in spotify"). But iOS will not deliver the universal link to myApp when I click it from within myApp.
After much testing, I discover that I can prevent the display of a webpage associated with the universal link by looking for the specific URL and cancelling the display:
func webView(webView: WKWebView, decidePolicyForNavigationResponse navigationResponse: WKNavigationResponse, decisionHandler: (WKNavigationResponsePolicy) -> Void) {
if let urlString = navigationResponse.response.URL?.absoluteString {
if urlString.hasPrefix(myULPrefix) { // it's a universal link targetted at myApp
decisionHandler(.Cancel)
return
}
}
decisionHandler(.Allow)
}
I have to do this in the decision handler for the response, not the one for the action. When I do this, the universal link is queued for delivery to myApp. BUT it is not actually delivered by iOS until I quit my app (for example, by hitting the home button) and relaunch it. The appDelegate function specified as delivering this message in the Apple docs referenced above
func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool {}
is not called. It is called when I do a more conventional deeplink - clicking a universal link in Safari to open myApp.
Is this a bug? Or a feature? Or am I, as usual, just barking up the wrong tree?
Thanks!
This will be the behavior if your wkwebview wep page domain name is same as your universal link domains name. In shot, If web page opened in Safari,SFSafariVC, WKWebView or UIWebView has domain same as your app's app link (universal link) domain it will not tirgger universal link,
I think Apple is made that this way because you can handle it more customizable when it happens when your application is running.
You found the place to put your own handler call:
func webView(webView: WKWebView, decidePolicyForNavigationResponse navigationResponse: WKNavigationResponse, decisionHandler: (WKNavigationResponsePolicy) -> Void) {
if let urlString = navigationResponse.response.URL?.absoluteString {
if urlString.hasPrefix(myULPrefix) { // it's a universal link targetted at myApp
decisionHandler(.Cancel)
// put here the call to handle the link, for example:
MyLinkHandler.handle(urlString)
return
}
}
decisionHandler(.Allow)
}
You can make any action in the background and the user stays on the opened page, or you can hide the web view and open another section of your application to show appropriate content.
I resolved this issue with my Swift 4 class below. It also uses embedded Safari Browser if possible. You can follow a similar method in your case too.
import UIKit
import SafariServices
class OpenLink {
static func inAnyNativeWay(url: URL, dontPreferEmbeddedBrowser: Bool = false) { // OPEN AS UNIVERSAL LINK IF EXISTS else : EMBEDDED or EXTERNAL
if #available(iOS 10.0, *) {
// Try to open with owner universal link app
UIApplication.shared.open(url, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : true]) { (success) in
if !success {
if dontPreferEmbeddedBrowser {
withRegularWay(url: url)
} else {
inAnyNativeBrowser(url: url)
}
}
}
} else {
if dontPreferEmbeddedBrowser {
withRegularWay(url: url)
} else {
inAnyNativeBrowser(url: url)
}
}
}
private static func isItOkayToOpenUrlInSafariController(url: URL) -> Bool {
return url.host != nil && (url.scheme == "http" || url.scheme == "https") //url.host!.contains("twitter.com") == false
}
static func inAnyNativeBrowser(url: URL) { // EMBEDDED or EXTERNAL BROWSER
if isItOkayToOpenUrlInSafariController(url: url) {
inEmbeddedSafariController(url: url)
} else {
withRegularWay(url: url)
}
}
static func inEmbeddedSafariController(url: URL) { // EMBEDDED BROWSER ONLY
let vc = SFSafariViewController(url: url, entersReaderIfAvailable: false)
if #available(iOS 11.0, *) {
vc.dismissButtonStyle = SFSafariViewController.DismissButtonStyle.close
}
mainViewControllerReference.present(vc, animated: true)
}
static func withRegularWay(url: URL) { // EXTERNAL BROWSER ONLY
if #available(iOS 10.0, *) {
UIApplication.shared.open(url, options: [UIApplication.OpenExternalURLOptionsKey(rawValue: "no"):"options"]) { (good) in
if !good {
Logger.log(text: "There is no application on your device to open this link.")
}
}
} else {
UIApplication.shared.openURL(url)
}
}
}