I am working on a SwiftUI app handling Universal Links.
At this point it is pretty much working the expected way, while I am looking through the debugger at what is happening. In other words when I click the link while the app is in the background, then it wakes up and all is OK.
The magic is happening in the following function.
func scene(_ scene: UIScene,
continue userActivity: NSUserActivity) {
print(#function)
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL,
let components = NSURLComponents(url: incomingURL,
resolvingAgainstBaseURL: true),
let path = components.path else {return}
let params = components.queryItems ?? [URLQueryItem]()
// ... more useful code for this app ...
.....
}
But on the other hand if I click the link when the app is just not running (not even in the background). Of course the debugger cannot be connected. Then the app fires up and starts, but the expected behavior (reacting according to the link) is not happening. Why is that?
I presume in such a case I should be dealing with the situation other than in:
func scene(_ scene: UIScene,
continue userActivity: NSUserActivity)
{......}
But I am not sure.
Any relevant tip will be very much appreciated.
That flow, when the app opens, is handled by other function. You can get the URL or the URL scheme (if you are also handling that kind of links) from connectionOptions.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let urlContext = connectionOptions.urlContexts.first {
handleDeepLinking(url: urlContext.url)
} else if let userActivity = connectionOptions.userActivities.first, let url = userActivity.webpageURL {
handleUniversalLinks(url: url)
}
}
Related
I'm implementing deep links in ios using universal links.
From what I can see, when the app is in the background, it's working,using the sceneDelegate method
func scene(_ scene: UIScene, continue userActivity: NSUserActivity)
however, if the app is not in the background, the deep link, just opens the app without sending the information of the link.
Is it possible to redirect the user to a specific place in the app using universal links or some other way if the app is not in the background, for example after a reboot or if the user killed the app?
Thanks
That delegate method will only be triggered when the user is the app is in the background, in order to make universal links work when the app is killed you need to trigger the willConnectTo delegate method:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let userActivity = connectionOptions.userActivities.first,
userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL?.absoluteString else {
return
}
handleUniversalLink(url: url, scene: scene)
}
I have faced the problem that I can't solve with Notifications and Deeplinks.
When opening a deeplink UIApplication used to send a UIApplication.didFinishLaunchingNotification with userInfo that contained UIApplicationLaunchOptionsURLKey with the URL that was opened.
So we could subscribe to it like this:
NotificationCenter.default.addObserver(forName: UIApplication.didFinishLaunchingNotification, object: nil, queue: nil) { (notification) in
print(notification.userInfo) // prints UIApplicationLaunchOptionsURLKey: url_that_opened_the_app
}
Now, after iOS 13 it does not happen: userInfo is nil.
So the question is: is there a way to receive a notification from Notification Center when app opens a deeplink?
*Thoughts: *
I think that it is caused by the fact that UISceneDelegate is in charge of opening deeplinks now, which is confirmed by the fact that if we remove the SceneDelegate, we can get our userInfo back.
I tried to find if any of SceneDelegate notifications provide us with such information, but they don't: both didActivateNotification and willConnectNotification give nothing.
Yes, you can find the same with SceneDelegate as well.
Here is a Sample I made.
Once you launch your app from deep links, you will receive connectionOptions in
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
}
You can get your URL as follows:
if let context = connectionOptions.urlContexts.first as? UIOpenURLContext {
print(context.url.absoluteString)
}
So your function will look like:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
if let context = connectionOptions.urlContexts.first {
print(context.url.absoluteString)
//Here you can pass this URL to your notification and perform your logic.
}
}
DeepLink I tested was: deeplinks://mycustomDeepLink
Also I changed the scheme to Wait for the Executable to Launch to check the initial launch thing from deep links.
Note: Sometimes debugger doesn't work for whatsoever reason. So, in my sample, I added an alert view in the ViewController.
My widget has several links that the user can click, the link are set up as follows:
Link(destination: URL(string: "widget://start")!)
Now I am able to detect the press in the scene delegate with the following function:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
if let item = URLContexts.first {
UserDefaults.standard.set(item.url.absoluteString, forKey: "URL")
print(item.url)
print(URLContexts)
}
}
However, that doesn't work when the app is closed. I tried putting this block of code everywhere, scene delegate, app delegate, but I just can't find a solution on how to detect the tap when the app is closed.
Is there a way to do that?
Add this
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if let item = connectionOptions.urlContexts.first {
UserDefaults.standard.set(item.url.absoluteString, forKey: "URL")
print(item.url)
print(URLContexts)
}
}
}
For those, who use only AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if let userActDic = launchOptions?[UIApplication.LaunchOptionsKey.userActivityDictionary] as? [String: Any],
let userActivity = userActDic["UIApplicationLaunchOptionsUserActivityKey"] as? NSUserActivity {
// Do with user activity
}
}
One solution is to wait a little before load url
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
...Your code
// Load the link, but set a timeout of X seconds to fix app crashing when loading deep link while app is NOT already running in the background.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.handleUniversalLink(url: url)
}
}
My issue
I'm implementing URL Schemes in my application and they're overall working fine when the app is in the foreground or the background. However, I've noticed that when it is completely closed and another app tries to access content using my URL (eg. app:page?image=1 ) which would normally work, it just opens the app but the content is never caught.
My Approach
I've set up code in both my AppDelegate and SceneDelegate methods
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:])
And
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
Desired behavior
It opens when the app is in the background, foreground or closed
Actual behavior
It only opens when in foreground or background
To handle incoming URLs we simply call this function in both the scene(_:willConnectTo:options:) and the scene(_:openURLContexts:) delegate methods:
If the App is closed:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
// Since this function isn't exclusively called to handle URLs we're not going to prematurely return if no URL is present.
if let url = connectionOptions.urlContexts.first?.url {
handleURL(url: url)
}
}
If the app is in background or foreground
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
// Get the first URL out of the URLContexts set. If it does not exist, abort handling the passed URLs and exit this method.
guard let url = URLContexts.first?.url else {
return NSLog("No URL passed to open the app")
}
handleURL(url: url)
}
You can revert to the following article for more about scene delegate and URL Schemes: Custom URL Schemes in iOS
Since your app is currently not running, it will be launched with those launch options. i.e. those options will be passed to willFinishLaunchingWithOptions: / didFinishLaunchingWithOptions: instead. Add you code to one of these methods.
For more information, read documentation about how to Respond to the Launch of Your App, or, more specifically Determine Why Your App Was Launched.
EDIT:
As commented by #paulw11 below, scene delegate works differently, and must be handled separately.
However, in Respond to Scene-Based Life-Cycle Events section, the last point is:
In addition to scene-related events, you must also respond to the
launch of your app using your UIApplicationDelegate object. For
information about what to do at app launch, see
Responding to the Launch of Your App
So I assume, we still need to handle launch in willdidFinishLaunchingWithOptions / didFinishLaunchingWithOptions.
Method scene(_ scene: UIScene, continue userActivity: NSUserActivity) doesn't get called when the app is launched after the user clicks on a universal link.
It works fine when already launched app opens again after the user clicks on the universal link. The sample code:
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL,
let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
let path = components.path else {
return
}
let params = components.queryItems ?? [URLQueryItem]()
print("path = \(path)")
print("params = \(params)")
}
I tried to use application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration, but it never gets called when the user clicks on the link:
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
if let scene = connectingSceneSession.scene, let userActivity = scene.userActivity {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
if let incomingURL = userActivity.webpageURL,
let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
let path = components.path {
let params = components.queryItems ?? [URLQueryItem]()
print("path = \(path)")
print("params = \(params)")
}
}
}
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
I tried to use scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions):
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let userActivity = scene.userActivity {
self.scene(scene, continue: userActivity)
}
}
I also tried the following methods:
func sceneDidBecomeActive(_ scene: UIScene) {
if let userActivity = scene.userActivity {
self.scene(scene, continue: userActivity)
}
}
func sceneWillEnterForeground(_ scene: UIScene) {
if let userActivity = scene.userActivity {
self.scene(scene, continue: userActivity)
}
}
But scene.userActivity is always nil there and I can't get userActivity.webpageURL.
How can we recognize that the link was clicked and the app was launched (not just opened)?
You almost had it:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let userActivity = scene.userActivity { // <-- not quite
self.scene(scene, continue: userActivity)
}
}
It's not in the scene; it's in the connectionOptions . Look in the connectionOptions.userActivities. (Though if what has happened is that the user clicked a link to launch us, I would expect to find the URL in the connectionOptions.urlContexts.)
The accepted answer by Matt works for launching universal links when the app isn't already opened.
If you also want to handle universal links when the app is opened, you need both functions shown below:
// SceneDelegate.swift
// This function is called when your app launches.
// Check to see if our app was launched with a universal link.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// See if our app is being launched via universal link.
for userActivity in connectionOptions.userActivities {
if let universalLink = userActivity.webpageURL {
// Do whatever you want with the universal link here.
// NOTE: if you're navigating a web view, know that the web view will not be initialized here yet.
// To navigate a web view, store the URL in a variable and navigate to it once the web view is initialized.
}
}
}
// SceneDelegate.swift
// This function is called when your app is already running and a universal link to your app is clicked.
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
// Ensure we're trying to launch a link.
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let universalLink = userActivity.webpageURL else {
return
}
// Handle the universal link here.
// If you're navigating a web view, here's how I do it:
//MyApp.webView.evaluateJavaScript("location.href = '\(universalLink)'")
}
I've verified this works for my app. See this Github thread for more details.
Apple responded confirming that issue in iOS 13.
This worked for me:
func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
for userActivity in connectionOptions.userActivities {
if let url = userActivity.webpageURL { //ADD WHATEVER CONDITION YOU NEED
//DO WHAT YOU NEED HERE
break
}
}
}
Basically the problem is that the universal link is "hidden" inside the connectionOptions so you have to search for it with the loop.