Swift 5 open universal link in WebView from SceneDelegate - ios

I have an app that can be open via universal links (from emails, chats, etc.).
When the app is open in background my code works as expected.
If the app is closed I can't pass the URL to my ViewController to open it in a WebView.
I'm trying to avoid the timeout workaround in the code below.
I've tried to get it via shared variable universalLink in my ViewController but I get a nil:
let universalLink = SceneDelegate.shared?.universalLink
SceneDelegate:
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
static var shared: SceneDelegate?
var universalLink: URL?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Universal link clicked when app was closed
for userActivity in connectionOptions.userActivities {
if let universalLink = userActivity.webpageURL {
// Workaround that I want to avoid:
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
self.openUniversalLink(userActivity: userActivity)
}
}
}
guard let _ = (scene as? UIWindowScene) else { return }
Self.shared = self
}
// Universal link clicked when the app is open in background (works fine).
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
openUniversalLink(userActivity: userActivity)
}
func openUniversalLink(userActivity: NSUserActivity)
{
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let universalLink = userActivity.webpageURL//,
else {
return
}
let appURL:[String: String] = ["appURL": universalLink.absoluteString]
let notificationName = Notification.Name("updateWebView")
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: appURL)
}
}

Related

Deep Links with AppDelegate and SceneDelegate

I am trying to implement deep links to navigate to posts on an app, it was an older project so I had to add the SceneDelegate class. The deep link implementation works only when the app is active or in background. If the app has not been loaded the deep link will not work. I've seen many posts and tutorials on this and have not found out why, has anyone had similar issues?
In the AppDelegate class I have added implementation to handle links for the following functions:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {}
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {}
In SceneDelegate I implement handling the links in the following functions:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {}
the implementation in those functions looks like this:
let navigator = Navigator()
navigator.getDesination(for: url)
func getDesination(for url: URL){
let destination = Destination(for: url)
let ivc = InstantiateViewController()
switch destination {
case .post(let postID):
ivc.openPostVC(id: postID, showComment: true, commentID: nil)
case .user(let userID):
ivc.openProfileVC(userID: userID)
default:
break
}
}
enum Destination {
case post(Int)
case user(Int)
case feed(String)
case store
case safari
init(for url: URL){
if(url.pathComponents[1] == "p"){
self = .post(Int(url.pathComponents[2])!)
} else if(url.pathComponents[1] == "user") {
self = .user(Int(url.pathComponents[2])!)
} else if(url.pathComponents[1] == "store") {
self = .store
} else if(url.pathComponents[1] == "s") {
self = .feed(url.pathComponents[2])
} else {
self = .safari
}
}
}
func openProfileVC(userID: Int){
let service = UserPool.shared.request(for: userID)
let storyboard = UIStoryboard(name: "Profile", bundle: nil)
let profileVC = storyboard.instantiateViewController(withIdentifier: "ProfileView") as! ProfileViewController
profileVC.userService = service
profileVC.shouldNavigateToHome = true
profileVC.shouldNavigateToHomeAction = {
self.loadMainStoryboard()
}
let navigationVC = UINavigationController(rootViewController: profileVC)
navigationVC.view.backgroundColor = .white
if #available(iOS 13.0, *) {
guard let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate else {return}
sceneDelegate.window?.rootViewController = navigationVC
sceneDelegate.window?.makeKeyAndVisible()
} else {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
appDelegate.window?.rootViewController = navigationVC
appDelegate.window?.makeKeyAndVisible()
}
}
The websites app-site-assocation file looks like this and have added associated domain in Xcode:
{"applinks":{"apps":[],"details":[{"appID":"{my ID}","paths":["*"]}]},"webcredentials":{"apps":["{my ID}"]}}
In iOS 13 and later with a scene delegate your app can observe the incoming universal link event at launch like this:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let url = connectionOptions.userActivities.first?.webpageURL {
// ... or might have to cycle thru multiple activities
}
}
If the app was already running you use this:
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
if let url = userActivity?.webpageURL {
// ...
}
}
(I have a very simple downloadable demo app, and it proves that this really does work. I do not understand the claim that it does not; perhaps the problem is a failure to understand how to test.)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
//---------
//-------
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
for context in URLContexts {
print("url: \(context.url.absoluteURL)")
print("scheme: \(context.url.scheme)")
print("host: \(context.url.host)")
print("path: \(context.url.path)")
print("components: \(context.url.pathComponents)")
}
}
}
from apple docs:
If your app has opted into Scenes, and your app is not running, the system delivers the URL to the scene(:willConnectTo:options:) delegate method after launch, and to scene(:openURLContexts:) when your app opens a URL while running or suspended in memory.
Full example:
In Scene delegate when app is terminated:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let url = connectionOptions.urlContexts.first?.url
}
and for when app is background or foreground:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
let url = URLContexts.first?.url
}
I didn't find an answer so I decided to work around the issue. I reverted back to AppDelegate only, in this situation Deep links only worked while the app was active or in background. To fix this I decided to store the URL in UserDefaults. So in the didFinishLaunchingWithOptions function I added the following:
if let url = launchOptions?[UIApplication.LaunchOptionsKey.url] as? URL {
UserDefaults.setURLToContinue(urlString: url.absoluteString)
} else if let activityDictionary = launchOptions?[UIApplication.LaunchOptionsKey.userActivityDictionary] as? [AnyHashable: Any] {
for key in activityDictionary.keys {
if let userActivity = activityDictionary[key] as? NSUserActivity {
if let url = userActivity.webpageURL {
UserDefaults.setURLToContinue(urlString: url.absoluteString)
}
}
}
}
Here is the UserDefaults extension I created:
extension UserDefaults {
class func setURLToContinue(urlString: String){
UserDefaults.standard.set(urlString, forKey: "continueURL")
}
class func getURLToContinue() -> String? {
return UserDefaults.standard.string(forKey: "continueURL")
}
class func removeURLToContinue(){
UserDefaults.standard.removeObject(forKey: "continueURL")
}
}
Lastly in the initial view controller's viewDidLoad function I handle the link:
if let urlString = UserDefaults.standard.string(forKey: "continueURL") {
let url = URL(string: urlString)!
let navigator = Navigator()
navigator.getDesination(for: url)
UserDefaults.removeURLToContinue()
}
Where the Navigator class decides what view controller to push on the navigation stack
Everything worked perfectly after this

iOS Deep Link from Widget when App closed?

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)
}
}

iOS 13 > Universal App Links does not work when app is killed and not in background

Any idea why starting from iOS 13 app links (universal links) this defines via Apple-App-Site-Association stopped working?
I have 2 implementations in ApplicationDelegate and in SceneDelegate.
Now works only implementation in SceneDelegate and only if application is in background, if I kill app then method continueUserActivity isn't called. I have added Haptic Feedback to track this method call but it will never be invoked neither in ActivityDelegate or SceneDelegate.
// MARK: - Universal Links support
extension SceneDelegate {
func scene(_ scene: UIScene, willContinueUserActivityWithType userActivityType: String) {
print("[Scene] Will continue user activity: ", userActivityType)
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
}
func scene(_ scene: UIScene, didFailToContinueUserActivityWithType userActivityType: String, error: Error) {
print("[Scene] Did fail to continue user activity: ", userActivityType)
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
print("[Scene] Application continue user activity...")
if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
if let url = userActivity.webpageURL {
if !present(url: url) { UIApplication.shared.open(url, options: [:]) }
}
}
}
And Application Delegate case
// MARK: - Universal Links support
extension AppDelegate {
func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool {
print("[App] Will continue user activity: ", userActivityType)
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.warning)
return true
}
func application(_ application: UIApplication, didFailToContinueUserActivityWithType userActivityType: String, error: Error) {
print("[App] Did fail to continue user activity: ", userActivityType)
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
print("[App] Application continue user activity...")
if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
if let url = userActivity.webpageURL {
if !present(url: url) { UIApplication.shared.open(url, options: [:]) }
}
}
return true
}
App is being opened but the methods are not called and I cannot navigate to appropriate screen inside my app.
Ok I've found it you must do something like this
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
   
    if let userActivity = connectionOptions.userActivities.first {
      self.scene(scene, continue: userActivity)
    }
  }
For me, only solution is to delete SceneDelegate and put code inside AppDelegate and it works, whenever the app is killed or in background.
If you don use SwiftUI you can delete SceneDelegate
For the SwiftUI 2.0 users, who don't have App or SceneDelegate. My example is about Firebase universal link, which should do something once it is opened. There is a very good method called onOpenURL which you can use.
var body: some Scene {
WindowGroup {
ContentView(shouldPresentThankYouView: $shouldPresentThankYouView).onOpenURL { url in
_ = DynamicLinks.dynamicLinks().handleUniversalLink(url) { (dynamicLink, error) in
guard error == nil else{
print("Found an error! \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink {
shouldPresentThankYouView = true
self.handleIncomingDynamicLink(dynamicLink)
}
}
}
}
}
If the user has the app installed, once he click on the app, a Thank You View will appear. Hope it helps you when working it universal links more specifically with Firebase links.

`scene(_ scene: UIScene, continue userActivity: NSUserActivity)` doesn't get called when the app is launched after the user clicks on a universal link

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.

How do you pass data from a custom url scheme to Views in SwiftUI?

There's no shortage of tips and tutorials on handling custom URL schemes in iOS. What ALL fail to do is actually show you how to pass data parsed from those URLs to your app/views. Yea, I can use a global variable, but that's not the "right" way and plus if you want your Swift view to react to a change in that global variable, you can't.
For example, I have,
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>){
let urlContext = URLContexts.first // Because I don't know how to properly deal with Sets
if let url = urlContext?.url{
guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
let params = components.queryItems else {
print("Invalid URL or album path missing")
return
}
if let token = params.first(where: { $0.name == "token" })?.value {
print("Token: \(token)")
MyGlobalToken = token
}
}
}
You'll see the MyGlobalToken option in there which works, but I can't respond to a change in that variable. Do I have to do something with the self.window?.rootViewController but I can't find any documentation on what to do. Or do you set up a "notification" so that you view responds? Or is this not implemented yet in SwiftUI?
FWIW I'm new to iOS development.
Here is a great blog to learn about SceneDelegate in iOS 13.
First answer is not a great answer.
If you run your app from a completely inactive state -- i.e. when you run it from XCode aka when it's restarted or not running in the background -- the app will call the func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) method to initialize the scenes infrastructure.
If you are running your app in the background when you point to the URL in Safari the app will call the func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) method.
In order to be thorough, you will have to attempt to get the url parameters in both methods. You can pass this query to the AppDelegate for future use by ViewControllers in both methods as well.
NOTE: If the first ViewController the app opens needs the URL query information you will need to do a few extra steps. To get your ViewControllers to actually update with the information, you will need to use the sceneDidBecomeActive() method for when the app is run from an inactive state/in the background. This method will have to call a method in the ViewController in order for it to pull the variable from the app delegate when the user enters your app. In this case I used viewDidLoad() methods to pull the updated variable from the AppDelegate.
Below is the full code for reference:
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
let appDelegate = UIApplication.shared.delegate as! AppDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
appDelegate.query = connectionOptions.urlContexts.first?.url.query ?? "No query"
guard let _ = (scene as? UIWindowScene) else { return }
}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
appDelegate.query = URLContexts.first?.url.query
}
func sceneDidDisconnect(_ scene: UIScene) {}
// Only needed if first ViewController needs updated AppDelegate Variable
func sceneDidBecomeActive(_ scene: UIScene) {
self.window?.rootViewController?.viewDidLoad()
}
func sceneWillResignActive(_ scene: UIScene) {}
func sceneWillEnterForeground(_ scene: UIScene) {}
func sceneDidEnterBackground(_ scene: UIScene) {
(UIApplication.shared.delegate as? AppDelegate)?.saveContext()
}
}
I had many trouble in this problem.
I don't know many things in IOS.
But There is no answer. I write it.
If you don't use sceneDelegate, You may use your global variable.
(I don't know why it doesn't work)
For this, I do like below.
Delete scenedelegate.swift.
delete sceneDelegate thing in Info.plist
initialLize check variable
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
UserDefaults.check = ""
return true
}
Add lines in Appdelegate for setting global variable.
open func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
//set Global variable
// ... You should add check url for your application ...
UserDefaults.check = "checked"
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
if let uservc = self.storyboard?.instantiateViewController(withIdentifier: "MainViewController") as? MainViewController
{
if #available(iOS 13.0, *) {
uservc.isModalInPresentation = true
}
uservc.debugText.text = "This is openUrl State"
self.present(uservc, animated: false) {
}
}
}
Make IntroViewController and MainViewController
IntroViewController is rootviewcontroller.
and MainViewController is second view controller.
if check variable is "checked", Viewdidload in IntroView is not excuted.
and this time is application-open is excuted with safari or chrome.
//IntroViewController
class IntroViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
if UserDefaults.check.elementsEqual("") {
if let uservc =
self.storyboard?.instantiateViewController(withIdentifier: "MainViewController") as? MainViewController
{
if #available(iOS 13.0, *) {
uservc.isModalInPresentation = true
}
uservc.debugText.text = "This is normal State"
self.present(uservc, animated: false) {
}
}
}
}
6.Global Variable
//UserDefaults.swift
import Foundation
fileprivate let keyCheck = "keyCheck"
extension UserDefaults {
static var check: String {
get {
return UserDefaults.standard.string(forKey: keyCheck) ?? ""
}
set(value) {
UserDefaults.standard.set(value, forKey: keyCheck)
UserDefaults.standard.synchronize()
}
}
}
When I use this logic in scenedelegate, It didn't work well.(I couldn't check "check variable".)

Resources