This is in a SwiftUI macOS app using the new App protocol and #main.
Usage flow:
User launches app and clicks a button which opens a particular webpage
Webpage eventually redirects to the app's URL scheme, opening the app and invoking onOpenURL(_:)
Expected behaviour:
The deep link is sent to the existing, currently open app instance
Actual behaviour:
A new app instance is launched, causing two instances of the app to be active
Note: There isn't really any code to add since the problem is just dependent on adding a URL scheme to the app and having a webpage go to it.
onOpenURL(_:) is not actually launching a new app instance, it is creating a new window within the existing instance. The documentation would suggest this only happens on macOS (because iOS only supports a single window).
You need to use the .handlesExternalEvents(preferring:allowing:) modifier on a higher order view. Calling handlesExternalEvents will override the default behaviour which is what is creating the new window in your app on macOS. Something like:
#main
struct myApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.handlesExternalEvents(preferring: ["myscheme"], allowing: ["myscheme"])
}
}
}
An then in a child view (e.g. ContentView()):
var body: some View {
VStack {
// your UI
}
.onOpenUrl{ url in
// do something with the deep link url
}
}
Related
TLDR: How to distinguish between a link that the app sends internally and a link that originates from outside of the app?
Details:
If a SwiftUI app has multiple windows (scenes), a window can be opened programmatically via a deep link as explained here.
Contextual data can be included in the link. For example a book id in a library app.
fancyLibraryApp:viewer/book/42
Add the following modifier to your view to process this url and to view a book.
var body: some Scene
{
Text("Title of \(book)").onOpenURL
{
self.id = $0.description.lastPathComponent //parse the url as appropriate
}
}
Book is a value or object that you retrieve from the id passed through the url.
The id should be a binding so that the view gets updated.
A link can be send from elsewhere within your app but it can also be send from an external source such as safari or the Terminal app (open fancyLibraryApp:viewer/book/42).
Therefore the url must be validated. Is the user allowed to read this book or not? Is the user logged in or not?
Apple explains how to validate an incoming url here, but only for iOS.
NSApplicationDelegate has a function application(_ application: NSApplication, open urls: [URL])
However that function is called after the view modifier onOpenURL.
So how can the url be verified?
I have a webview inside my application and when an external link is clicked (that in normal browser is open in a new tab), I can't then go back to my website.
It is possible when a new tab is open to have the menu closed that tab like Gmail do ?
The objective is that, whenever a link is clicked, the user would have the choice to choose which option to view the content with, e.g. Clicking a link would suggest open youtube app or google chrome. The purpose is to appear the google chrome option
Or what suggestions do you have to handle this situation ?
If I understood you correctly, you want to have the option to select how to open the web link - inside your app, or within another app's (browser) context.
If this is correct, then you can use Xamarin.Essentials: Browser functionality.
public async Task OpenBrowser(Uri uri)
{
await Browser.OpenAsync(uri, BrowserLaunchMode.SystemPreferred);
}
Here the important property is the BrowserLaunchMode flag, which you can learn more about here
Basically, you have 2 options - External & SystemPreferred.
The first one is clear, I think - it will open the link in an external browser.
The second options takes advantage of Android's Chrome Custom Tabs & for iOS - SFSafariViewController
P.S. You can also customise the PreferredToolbarColor, TitleMode, etc.
Edit: Based from your feedback in the comments, you want to control how to open href links from your website.
If I understood correctly, you want the first time that you open your site, to not have the nav bar at the top, and after that to have it. Unfortunately, this is not possible.
You can have the opposite behaviour achieved - the first time that you open a website, to have the nav bar and if the user clicks on any link, to open it externally (inside a browser). You have 2 options for this:
To do it from your website - change the a tag's target to be _blank like this;
To do it from your mobile app - create a Custom renderer for the WebView. In the Android project's renderer implementation, change the Control's WebViewClient like so:
public class CustomWebViewClient : WebViewClient
{
public override bool ShouldOverrideUrlLoading(Android.Webkit.WebView view, IWebResourceRequest request)
{
Intent intent = new Intent(Intent.ActionView, request.Url);
CrossCurrentActivity.Current.StartActivity(intent);
return true;
}
}
It seems that Catalina will ask the user for permissions for an app every time anything changes in the Xcode build.
For example, I have a simple example that asks for permission to access the microphone. The first time the app is compiled and run, it asks for microphone access. I grant that access. If I build and run again without changing anything in the code, that access will be retained. However, if I change any code (even changing the Text label by one character), the permission dialog is displayed again.
import AVFoundation
import SwiftUI
struct ContentView : View {
#ObservedObject private var recorderManager = AudioRecorderManager()
var body: some View {
Text("Recording view")
}
}
class AudioRecorderManager: NSObject, ObservableObject {
override init() {
super.init()
AVCaptureDevice.requestAccess(for: .audio) { allowed in
DispatchQueue.main.async {
if allowed {
print("Allowed")
} else {
print("Not allowed")
}
}
}
}
}
Note that this also requires you to add Audio Input to the entitlements and NSMicrophoneUsageDescription to the Info.plist before it'll run.
I've also noticed the same phenomenon happening with accessing Keychain data.
This is dramatically slowing down my development time having to confirm access (or worse, enter my password for the Keychain data) every single time I build the app. Is there a solution to get Catalina/Xcode to give lasting permission to the app during development?
I know this sounds weird. Is there any way we can open a URI from background tasks in Windows 10 Apps?
I have 2 requirements,
Talk to cortana and it will show you results based on the speech recognition, when user clicks on it, we cannot open the links in browser directly. Instead I am passing the Launch Context to the Foreground app and then using LauchUri I am opening the url in default browser.
Send toast notifications from the App, when user clicks on it, I have requirement to open a url instead opening an app. So, did the same, by passing the launch context to foreground app and then opening the url.
Both scenarios, it just opening url in browser. Here user experience is very poor that user seeing the app open for each action and then opening browser. Please throw some ideas if any possibilities.
thanks in advance.
For your second requirement, you can make Toast Notifications launch a URL!
If you're using the Notifications library (the NuGet package that we suggest you use), just set the Launch property to be a URL, and change the ActivationType to Protocol. You can also do this with raw XML, but that's error-prone.
You can also make buttons on the toast launch a URL too, since they also support ActivationType of Protocol.
Show(new ToastContent()
{
Visual = new ToastVisual()
{
BindingGeneric = new ToastBindingGeneric()
{
Children =
{
new AdaptiveText() { Text = "See the news" },
new AdaptiveText() { Text = "Lots of great stories" }
}
}
},
Launch = "http://msn.com",
ActivationType = ToastActivationType.Protocol
});
Some details:
I am not enrolled into Mac Developer program, hence the code is not signed.
I am not sandboxing the app.
It's a status bar only app, no dock or menu will appear.
Here's what I did:
Added a new project for helper app on top my existing main app project.
In main app settings:
Added ServiceManagement.framework framework to my main app.
Set Strip Debug Symbols During Copy to NO
In Build Phases tab, under Copy Files, added Contents/Library/LoginItems as Subpath. And added my Helper App.app
For helper app settings:
Set Application is background only to YES
Under Build Settings, set Skip Install to YES
Hocus Pocus.app is my main app and Hocus Pocus Helper.app is helper app.
In AppDelegate of helper app, my code is:
func applicationDidFinishLaunching(aNotification: NSNotification) {
NSWorkspace.sharedWorkspace().launchApplication("Hocus Pocus.app")
NSApplication.sharedApplication().terminate(self)
}
In AppDelegate of main app:
...
override init() {
self.mainBundle = NSBundle.mainBundle()
let path = mainBundle.bundlePath.stringByAppendingPathComponent(
"Contents/Library/LoginItems/Hocus Pocus Helper.app")
self.helperBundle = NSBundle(path: path)!
super.init()
}
...
...
func makeThisAppStartAtLogin(state: Int) {
let result = SMLoginItemSetEnabled(helperBundle.bundleIdentifier!, Boolean(state))
if result != 0 {
println("success")
}
else {
println("failed")
}
}
It's not working when I call makeThisAppStartAtLogin(1), why?
Apple documentation mentions:
The Boolean enabled state of the helper application. This value is effective only for the currently logged in user. If true, the helper application will be started immediately (and upon subsequent logins) and kept running. If false, the helper application will no longer be kept running.
When I call makeThisAppStartAtLogin(_:), it should start helper app immediately. But it doesn't seem to be doing that.
The if block prints success.
In Helper App code, I also sent the path to launchApplication(_:) instead of app name, it did not make any difference.
I have ran this app from default debug directory and also under /Applications
I have also tried following, it didn't make any difference:
...
let launchDaemon: CFStringRef = "avi.Hocus-Pocus-Helper"
if SMLoginItemSetEnabled(launchDaemon, Boolean(state)) {
...
I have checked that helper app is copied to main app at correct path.
When I launch the helper app manually, either from debug folder or from main app's Contents, it does launches main app and kill itself, as it should.
Here's the full repository, if you are interested. Check loginitems branch.