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
Related
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)
}
}
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)
}
}
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.
im implementing deep link is iOS. I have configured the URL Scheme in Project-Setting->Info->Url type
URL Schemes : carwash role:Viewer
when I type carwash://something the browser asks for opening the application but nothing get called in Application that I handle that what action should occur .
apple documentation says you should override application(open url) in AppDelegate but deep link dosent call it and the application opens in last state
application:openURL:options:' is not being called
this is my code and dose not work
func application(_ app: UIApplication, open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
fatalError()
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if let url = launchOptions?[UIApplication.LaunchOptionsKey.url] as? URL {
/// some
fatalError()
}
GMSServices.provideAPIKey("")
return true
}
In iOS 13, UIApplication application(_:open:options:) doesn't get called (at least in my case maybe).
You should override the SceneDelegate functions below instead:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
if let url = URLContexts.first?.url{
print(url)
}
}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// detect the URLContext in the `options:` and deal with it.
}
If the user taps the link when your app isn't running, scene(_: openURLContexts:) won't be called but scene(_:willConnectTo:options:) will be.
in iOS 13+ or scene delegate there two methods will call and willPerformHTTPRedirection method will definitely call.
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL {
}
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Swift.Void) {
if let url = request.url {
guard let detailDictionary = String.queryParameters(from: url) else { return }
}
}
}
Parse url by this function:
static func queryParameters(from url: URL) -> [String: String]? {
let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
var queryParams: [String : String]? = nil
if let components = urlComponents?.queryItems {
queryParams = [String: String]()
for queryItem in components {
if queryItem.value == nil {
continue
}
queryParams?[queryItem.name] = queryItem.value
}
}
return queryParams
}
You should use application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool
Note that you have no return statement in your method
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
print("Got from URL: \(url)"
return true
}
Links in simulator best to be done through terminal
xcrun simctl openurl booted “carwasher://deeplink”
For me in iOS13+ the following works (in my SceneDelegate):
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Set up everything you need
// ...
// Handle deep link on cold start
if let url = connectionOptions.userActivities.first?.webpageURL {
handle(url: url)
}
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
if let url = userActivity.webpageURL {
// Handle deep link when the app is running
handle(url: url)
}
}
You should configure your deeplinks in Info.plist file
Example:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>carwash</string> // your identifier
<key>CFBundleURLSchemes</key>
<array>
<string>carwash</string> // schema
</array>
</dict>
</array>
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".)