Support URL schemes in macOS application - swift

There are several (old) questions on this subject, but none of the solutions worked for me, so here's the question:
How to add a URL scheme, so it will be possible to open my app via the browser?
I did the following:
Added the required info to the info.plist:
I Added those functions:
func applicationWillFinishLaunching(_ notification: Notification) {
NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(handleEvent(_:with:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
}
#objc func handleEvent(_ event: NSAppleEventDescriptor, with replyEvent: NSAppleEventDescriptor) {
NSLog("at handleEvent")
}
I also tried to add this function:
func application(_ application: NSApplication, open urls: [URL]) {
for url in urls {
NSLog("url:\(url)")
}
}
None of the above worked. I have a webpage that redirect with MyAppLogin://test but nothing happens. It doesn't matter if the app is open or closed (I want it to work in both cases)
Any idea what's the problem here?
Edit: Two more details:
The app is sandboxed
I'm running it via Xcode (so the installation is not at the 'Applications' folder)

One year later, this is how I made it work:
Add this to your AppDelegate:
func applicationWillFinishLaunching(_ notification: Notification) {
let appleEventManager: NSAppleEventManager = NSAppleEventManager.shared()
appleEventManager.setEventHandler(self, andSelector: #selector(handleGetURLEvent(_:withReplyEvent:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
}
Add this, to parse the URL and do whatever you want:
#objc func handleGetURLEvent(_ event: NSAppleEventDescriptor, withReplyEvent: NSAppleEventDescriptor) {
if let urlString = event.forKeyword(AEKeyword(keyDirectObject))?.stringValue {
let url = URL(string: urlString)
guard url != nil, let scheme = url!.scheme else {
//some error
return
}
if scheme.caseInsensitiveCompare("yourSchemeUrl") == .orderedSame {
//your code
}
}
}

Related

Flutter IOS Universal Link Opens App, But Does Not Navigate To Correct Page

I have set up Universal Links on my flutter project for IOS.
Like the title suggests, my app does open when I click on a link relating to my site but it does not navigate to the correct page. It just opens the app.
I'm not using the uni_links package, rather I used a combination of guides (including official documentation):
https://developer.apple.com/videos/play/wwdc2019/717/
https://nishbhasin.medium.com/apple-universal-link-setup-in-ios-131a508b45d1
https://www.kodeco.com/6080-universal-links-make-the-connection
I have setup my apple-app-site-association file to look like:
{
"applinks": {
"details": [
{
"appIDs": [
"XXXXXXX.com.my.appBundle"
],
"componenents": [
{
"/": "/*"
}
]
}
]
}
}
and I have added this to my info.plist file:
<key>FlutterDeepLinkingEnabled</key>
<true/>
and my AppDelegate.swift file looks like:
import UIKit
import Flutter
import Firebase
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
override func application(_ application: UIApplication, continue userActivity: NSUserActivity,
restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// This will allow us to check if we are coming from a universal link
// and get the url with its components
// The activity type (NSUserActivityTypeBrowsingWeb) is used
// when continuing from a web browsing session to either
// a web browser or a native app. Only activities of this
// type can be continued from a web browser to a native app.
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL,
let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
return false
}
// Now that we have the url and its components,
// we can use this information to present
// appropriate content in the app
return true
}
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
FirebaseApp.configure()
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
My Runner-entitlements are also setup correctly like:
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:www.example.com</string>
<string>applinks:*.example.com</string>
</array>
The issue is, if I click a hyperlink for www.example.com/mypath , it does not got to the page/route handled by /mypath, but instead just opens the app.
My routing is done using go_router: ^5.2.4
Please does anyone know why this is happening? I'm blocked by this. I have seen similar questions, but none with answers that have worked for me. Any help is appreciated.
Ok so figured it out. The official apple documentation requests the addition of a variation of this function in the AppDelegate.swift file:
func application(_ application: UIApplication, continue userActivity: NSUserActivity,
restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// This will allow us to check if we are coming from a universal link
// and get the url with its components
// The activity type (NSUserActivityTypeBrowsingWeb) is used
// when continuing from a web browsing session to either
// a web browser or a native app. Only activities of this
// type can be continued from a web browser to a native app.
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL,
let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
return false
}
// Now that we have the url and its components,
// we can use this information to present
// appropriate content in the app
return true
}
Seems that it conflicts with the flutter framework for handling universal links. Taking that function out and just having this in my info.plist worked (everything else stayed the same):
<key>FlutterDeepLinkingEnabled</key>
<true/>
Flutter documentation is not out for this (as at the time of posting this answer) so if people are interested, I could do a small article on the necessary steps.
When you handle the dynamic link you get the universal link and other data in the userActivity parameter of the following function.
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if let incomingURL = userActivity.webpageURL {
debugPrint("incoming url is", incomingURL)
let link = DynamicLinks.dynamicLinks().shouldHandleDynamicLink(fromCustomSchemeURL: incomingURL)
print(link)
let linkHandle = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL) { link, error in
guard error == nil else {
print("Error found.")
return
}
if let dynamicLink = link {
self.handleDynamicLinks(dynamicLink)
}
}
if linkHandle {
return true
} else {
return false
}
}
return false
}
Parse the data from another function or you can parse in above code also. In my case I parsed the code in below function.
func handleDynamicLinks(_ dynamicLink: DynamicLink) {
guard let link = dynamicLink.url else {
return
}
if let landingVC = self.window?.rootViewController as? LandingViewController {
// Do you your handling here with any controller you want to send or anything.
}
// example you are getting ID, you can parse it here
if let idString = link.valueOf("id"), let id = Int.init(idString) {
print(id)
}
}
When you get the details from the link you can simply fetch the navigation controller or the VisibleController, and then can push to the desired flow.

Give a new url to the browser with Swift macOS

I could find a way to open a specific browser (with macOS and Swift):
#IBAction func frx(_ sender: NSButton) {
NSWorkspace.shared.open(URL(fileURLWithPath: "/Applications/Firefox.app"))
}
Is it possible to give to that Firefox window a new url in a posterior moment and reload the page? (Give the address not when I launch the application but later)
struct Firefox {
static func open(path: String) {
let ff_url = NSURL(fileURLWithPath: "/Applications/Firefox.app", isDirectory: true) as URL
if let www_url = URL(string: path) {
NSWorkspace.shared.open([www_url], withApplicationAt: ff_url, configuration: NSWorkspace.OpenConfiguration()) { app, error in
if let error = error {
// handle error
}
if let _ = app {
// handle success
}
}
} else {
// handle error
}
}
}

Unable to inject JS into WKWebView in Swift/Cocoa/NextStep / Push user selection on web page in WKWebView to Swift / Cocoa

I'm working with an MacOS app which needs to use the WKUserScript capability to send a message from the webpage back to the MacOS app. I'm working with the article https://medium.com/capital-one-tech/javascript-manipulation-on-ios-using-webkit-2b1115e7e405 which shows this working in iOS and works just fine.
However I've been struggling for several weeks to try to get it to work in my MacOS. Here is my example of his code which complies fine and runs but does not successfully print the message found in the handler userContentController()
import Cocoa
import WebKit
class ViewController: NSViewController, WKNavigationDelegate {
#IBOutlet weak var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let userContentController = WKUserContentController()
// Add script message handlers that, when run, will make the function
// window.webkit.messageHandlers.test.postMessage() available in all frames.
userContentController.add(self, name: "test")
// Inject JavaScript into the webpage. You can specify when your script will be injected and for
// which frames–all frames or the main frame only.
let scriptSource = "window.webkit.messageHandlers.test.postMessage(`Hello, world!`);"
let userScript = WKUserScript(source: scriptSource, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
userContentController.addUserScript(userScript)
// let config = WKWebViewConfiguration()
// config.userContentController = userContentController
// let webView = WKWebView(frame: .zero, configuration: config)
webView.navigationDelegate = self
webView.configuration.userContentController = userContentController
// Make sure in Info.plist you set `NSAllowsArbitraryLoads` to `YES` to load
// URLs with an HTTP connection. You can run a local server easily with services
// such as MAMP.
let htmlStr = "<html><body>Hello world - nojs</body></html>"
webView.loadHTMLString(htmlStr, baseURL: nil)
}
}
extension ViewController: WKScriptMessageHandler {
// Capture postMessage() calls inside loaded JavaScript from the webpage. Note that a Boolean
// will be parsed as a 0 for false and 1 for true in the message's body. See WebKit documentation:
// https://developer.apple.com/documentation/webkit/wkscriptmessage/1417901-body.
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if let messageBody = message.body as? String {
print(messageBody)
}
}
}
Another odd thing is that I do not seem to be able to create a simple WKWebView app that loads a page and displays it. These are all just simple tests and my main application is able to load/display webpages just fine using AlamoFire/loadHTMLString() to display pages, I just have not been able to inject the JS required.
Everything I've done in the conversion is quite straight forward and required little or no change with the exception of the assignment of the userContentController - so perhaps that's the problem? This example works just fine in iOS with his original sample as a prototype. https://github.com/rckim77/WKWebViewDemoApp/blob/master/WKWebViewDemoApp/ViewController.swift
I'm guessing there must be something very simple I'm missing here. Any help would be greatly appreciated!
Heres how I have set my WebView on Mac try something like this
import Cocoa
import WebKit
class ViewController: NSViewController {
#IBOutlet weak var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let javascript = """
function printStatement() {
try {
window.webkit.messageHandlers
.callbackHandler.postMessage({'payload': 'Hello World!'})
} catch(err) {
console.log('The native context does yet exist')
}
}
"""
let script = WKUserScript(
source: javascript,
injectionTime: WKUserScriptInjectionTime.atDocumentEnd,
forMainFrameOnly: true
)
webView.configuration.userContentController.add(
name: "callbackHandler"
)
webView.configuration.userContentController
.addUserScript(script)
webView.navigationDelegate = self
let html = """
<div onClick='javascript:printStatement()'>Print Statement</div>
"""
webView.loadHTMLString(html, nil)
}
}
extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if(message.name == "callbackHandler") {
guard let body = message.body as? [String: Any] else {
print("could not convert message body to dictionary: \(message.body)")
return
}
guard let payload = body["payload"] as? String else {
print("Could not locate payload param in callback request")
return
}
print(payload)
}
}
}
Hopefully this answered your question and works if not let me know and i'll try figure it out!
Well, as it turns out a major part of the issue was that I needed to set the entitlements for both "App Sandbox" and "com.apple.security.files.user-selected.read-only" both to "no" in the WebTest.entitlements file.
This was not the case in previous versions of XCode (I'm on V10.1) and the default values basically disabled the WKWebView for what I was trying to do with it (ie, load a simple page either via URL or String)
However, Alex's fix did help once I got that solved... with a couple small tweaks (had to add 'self' to the userContentController.add() function. Also, I added my JS for it's original purpose which was to "push" to Swift every time the user changed the selection on the page.
Here's my final code:
import Cocoa
import WebKit
class ViewController: NSViewController, WKNavigationDelegate {
#IBOutlet var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let javascript = """
function printStatement() {
try {
var foo = window.getSelection().toString()
window.webkit.messageHandlers.callbackHandler.postMessage({'payload': foo})
} catch(err) {
console.log('The native context does yet exist')
}
}
function getSelectionAndSendMessage() {
try {
var currSelection = window.getSelection().toString()
window.webkit.messageHandlers.callbackHandler.postMessage({'payload': currSelection})
} catch(err) {
console.log('The native context does yet exist')
}
}
document.onmouseup = getSelectionAndSendMessage;
document.onkeyup = getSelectionAndSendMessage;
document.oncontextmenu = getSelectionAndSendMessage;
"""
let script = WKUserScript(
source: javascript,
injectionTime: WKUserScriptInjectionTime.atDocumentEnd,
forMainFrameOnly: true
)
webView.configuration.userContentController.add(self, name: "callbackHandler")
webView.configuration.userContentController.addUserScript(script)
webView.navigationDelegate = self
let html = """
<div onClick='javascript:printStatement()'>Print Statement</div>
This is some sample text to test select with
"""
webView.loadHTMLString(html, baseURL: nil)
}
}
extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if(message.name == "callbackHandler") {
guard let body = message.body as? [String: Any] else {
print("could not convert message body to dictionary: \(message.body)")
return
}
guard let payload = body["payload"] as? String else {
print("Could not locate payload param in callback request")
return
}
print(payload)
}
}
}
Thanks Alex for all your fantastic support!

How to get url from webview whenever user move to other pages

How can I get a URL from webView whenever the URL is changed?
I want to change the button color when current URL is changed. So I need to check the current URL all the time.
And I also would like to get URL value as a String.
I tried the below code but it doesn't work at all.
NotificationCenter.default.addObserver(self, selector: #selector(urlChecker), name: NSNotification.Name.NSURLCredentialStorageChanged, object: webView)
you can use:
1. Solution
UIWebViewDelegate
https://developer.apple.com/reference/uikit/uiwebviewdelegate/1617945-webview
optional func webView(_ webView: UIWebView,
shouldStartLoadWith request: URLRequest,
navigationType: UIWebViewNavigationType) -> Bool
UIWebViewNavigationType:
https://developer.apple.com/reference/uikit/uiwebviewnavigationtype
don't forget to return true
case linkClicked
User tapped a link.
case formSubmitted
User submitted a form.
case backForward
User tapped the back or forward button.
case reload
User tapped the reload button.
case formResubmitted
User resubmitted a form.
case other
Some other action occurred.
2. Solution
Inject Javascript JavaScript MessageHandler
(credit to Vasily Bodnarchuk)
Solution is here: https://stackoverflow.com/a/40730365/1930509
Swift 3 example.
Description
The script is inserted into page which will displayed in WKWebView.
This script will return the page URL (but you can write another
JavaScript code). This means that the script event is generated on the
web page, but it will be handled in our function:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {...}
Full Code example
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
var webView = WKWebView()
let getUrlAtDocumentStartScript = "GetUrlAtDocumentStart"
let getUrlAtDocumentEndScript = "GetUrlAtDocumentEnd"
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
config.addScript(script: WKUserScript.getUrlScript(scriptName: getUrlAtDocumentStartScript),
scriptHandlerName:getUrlAtDocumentStartScript, scriptMessageHandler:
self, injectionTime: .atDocumentStart)
config.addScript(script: WKUserScript.getUrlScript(scriptName: getUrlAtDocumentEndScript),
scriptHandlerName:getUrlAtDocumentEndScript, scriptMessageHandler:
self, injectionTime: .atDocumentEnd)
webView = WKWebView(frame: UIScreen.main.bounds, configuration: config)
webView.navigationDelegate = self
view.addSubview(webView)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
webView.loadUrl(string: "http://apple.com")
}
}
extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
switch message.name {
case getUrlAtDocumentStartScript:
print("start: \(message.body)")
case getUrlAtDocumentEndScript:
print("end: \(message.body)")
default:
break;
}
}
}
extension WKUserScript {
class func getUrlScript(scriptName: String) -> String {
return "webkit.messageHandlers.\(scriptName).postMessage(document.URL)"
}
}
extension WKWebView {
func loadUrl(string: String) {
if let url = URL(string: string) {
load(URLRequest(url: url))
}
}
}
extension WKWebViewConfiguration {
func addScript(script: String, scriptHandlerName:String, scriptMessageHandler: WKScriptMessageHandler,injectionTime:WKUserScriptInjectionTime) {
let userScript = WKUserScript(source: script, injectionTime: injectionTime, forMainFrameOnly: false)
userContentController.addUserScript(userScript)
userContentController.add(scriptMessageHandler, name: scriptHandlerName)
}
}
Info.plist
add in your Info.plist transport security setting
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Result
Resources ##
Document Object Properties and Methods

Swift App Auto-Login not working

I followed this tutorial here to create an auto-login feature for ma Swift app: https://theswiftdev.com/2015/09/17/first-os-x-tutorial-how-to-launch-an-os-x-app-at-login/
The tutorial is pretty straightforward - the only problem is that I simply can not make it work with my app - it just doesn't work.
I have created a new project for the auto-login with the identifier "com.sascha-simon.com.NetWorkerAutoStarter".
func applicationDidFinishLaunching(_ aNotification: Notification)
{
let mainIdentifier = "com.sascha-simon.Mac.NetWorker"
let running = NSWorkspace.shared().runningApplications
var alreadyRunning = false
for app in running
{
if app.bundleIdentifier == mainIdentifier
{
alreadyRunning = true
break
}
}
if !alreadyRunning
{
DistributedNotificationCenter.default().addObserver(self, selector: #selector(AppDelegate.terminate), name: NSNotification.Name("killme"), object: mainIdentifier)
let path = Bundle.main.bundlePath as NSString
var components = path.pathComponents
components.removeLast()
components.removeLast()
components.removeLast()
components.append("MacOS")
components.append("NetWorker")
let newPath = NSString.path(withComponents: components)
NSWorkspace.shared().launchApplication(newPath)
}
else
{
self.terminate()
}
}
func applicationWillTerminate(_ aNotification: Notification)
{
// Insert code here to tear down your application
}
func terminate()
{
NSApp.terminate(nil)
}
My Main-App has the identifier "com.sascha-simon.NetWorker"
AppDelegate:
func applicationDidFinishLaunching(_ aNotification: Notification)
{
let starterIdentifier = "com.sascha-simon.NetWorkerAutoStarter"
var startedAtLogin = false
for app in NSWorkspace.shared().runningApplications
{
if app.bundleIdentifier == starterIdentifier
{
startedAtLogin = true
break
}
}
if startedAtLogin
{
DistributedNotificationCenter.default().post(name: NSNotification.Name("killme"), object: Bundle.main.bundleIdentifier!)
}
}
I have a NSPopup with a checkbox to enable the auto login:
#IBAction func startOSCheckedChange(_ sender: NSButton)
{
let value = sender.state == 1
SMLoginItemSetEnabled("com.sascha-simon.NetWorkerAutoStarter" as CFString, value)
}
The method returns true. I have exported the developer-id signed app, started it, enabled the auto login, logged out...and the app didn't start.
I have set an alert to the auto start project and the problem is, that it seems that the project is not started (and therefore the main project isn't started).
First I thought that I have to add a storyboard/MainMenu.xib file to the project (and add the key to the info.plist) but that didn't help either.
What could I have forgotten?