I've implemented Universal Links in my app and it opens when specific links are clicked in emails, messages etc.
Now I'm trying to handle this events but with no success at this point.
Any ideas what am I doing wrong?
Here's a part of what I've tried so far in my AppDelegate:
import Firebase
import FirebaseMessaging
import UserNotificationsUI
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate, NSUserActivityDelegate {
// ... register for notifications
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool
{
print("Test 1") // Doesn't print anything
return true
}
func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool
{
print("Test 2") // Doesn't print anything
return true
}
}
extension AppDelegate: UNUserNotificationCenterDelegate {
// ... handle notifications
}
P.S. I'm using Swift 5 on xCode 14
I had the same problem, the problem is that you have a SceneDelegate, because of this the AppDelegate methods are not called.
Implement the following methods in your SceneDelegate to handle Universal Links when the app is already running and when it is not launched yet:
//This method is called when app is NOT running in the background.
//Easy to test with fatalError()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
if let userActivity = connectionOptions.userActivities.first {
debugPrint("userActivity: \(userActivity.webpageURL)")
fatalError()
}
}
//This method is called when app is running in the background.
//Easy to test with debugPrint
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
debugPrint("userActivity: \(userActivity.webpageURL)")
}
From there, do whatever you need to handle the links.
Hope it helps :)
Related
If I never actually leave the current app for another app - and the screen turns off or the device gets locked.
Will lifecycle methods like applicationDidEnterBackground, applicationWillEnterForeground, etc still get called?
Basically my question is: does screen lock/device sleep behave the same way as switching to another app in terms of lifecycle methods.
Yes, except that in a modern app all of that stuff has been moved to the scene delegate. If the screen is locked and you are frontmost, you will get:
sceneWillResignActive(_:)
sceneDidEnterBackground(_:)
And then when the screen is unlocked you will get:
sceneWillEnterForeground(_:)
sceneDidBecomeActive(_:)
FWIW, if you want to diagnose these sorts of app lifecycle events yourself in the future, you can use Logger or os_log and watch these iOS events on your macOS Console app.
Note, I am not relying on the Xcode debugger, because having an app attached to the debugger alters the app lifecycle. I am explicitly not running the app through the Xcode debugger, but rather running it directly on the device having previously quit Xcode on my Mac.
In short, yes, simply turning off a phone (but not powering it down completely) puts your app into background mode. Here is the log where I launched the app, and then tapped on the power button on the side of my iPhone to turn it off:
This behavior is very similar to the following log where I just swiped up to return to the home screen:
But if you completely power down the device while the app is running, you will see it sceneWillResignActive, but unsurprisingly, it never gets to sceneDidEnterBackground:
Nor if you force-quit while the app is active:
For more information about logging an app and observing these from the macOS Console, see WWDC 2020’s Explore logging in Swift or WWDC 2016 Unified Logging and Activity Tracing for these logging options.
FWIW, here is my logging:
// AppDelegate.swift
import UIKit
import os
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "AppDelegate")
#main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
logger.debug(#function)
...
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
logger.debug(#function)
...
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
logger.debug(#function)
...
}
}
And:
// SceneDelegate.swift
import UIKit
import os
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "SceneDelegate")
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
logger.debug(#function)
...
guard let _ = (scene as? UIWindowScene) else { return }
}
func sceneDidDisconnect(_ scene: UIScene) {
logger.debug(#function)
...
}
func sceneDidBecomeActive(_ scene: UIScene) {
logger.debug(#function)
...
}
func sceneWillResignActive(_ scene: UIScene) {
logger.debug(#function)
...
}
func sceneWillEnterForeground(_ scene: UIScene) {
logger.debug(#function)
...
}
func sceneDidEnterBackground(_ scene: UIScene) {
logger.debug(#function)
...
}
}
If you are using os_log instead of Logger (the latter introduced in macOS 11 and iOS 14), the process is very similar. You will see os_log messages in the console in the same manner.
When I tap on the result of the Core Spotlight search, the app launches, but the function in AppDelegate.swift doesn't get called:
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
print("called")
}
which contains a logic for navigating to a specific view controller and loading data.
Some people here seem to recommend using SceneDelegate.swift:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
print("called"
}
but doesn't get called from Core Spotlight, either.
The only functions that seem to be being called are:
func sceneDidBecomeActive(_ scene: UIScene) {}
func sceneWillEnterForeground(_ scene: UIScene) {}
in SceneDelegate.swift, but doesn't have the needed userActivity parameter.
As per my App project setup,
I have following function calls with same code to instantiate rootVCs in SceneDelegate and AppDelegate respectively
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
}
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]?)
-> Bool {
}
In order to Implement Universal Links, I have the following callback function in my App Delegate
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
//code to capture and setup universal link
}
This function from AppDelegate is only called in less than iOS 13 devices.I looked for similar callback equivalent for SceneDelegate, The closest I could find was this function.
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
//code to capture and setup universal link
}
Configuration: Xcode Version 11.5 target iOS 10+ devices.
Problem: This particular callback is only called when there is an instance of the app running before Link is clicked. i.e. Once the App instance is killed, this function from SceneDelegate is not called and universal links Do not work for iOS13+ Devices.
I tried following this Xcode 11 - Opt out of UISceneDelegate/SwiftUI on iOS 13 to remove the Scene Delegate altogether, However ended up with only Black Screen.
Question: What am I doing wrong and what is the possible fix?
I had the same problem, the problem is that you have a SceneDelegate, because of this the AppDelegate methods are not called. So you are missing one method in your SceneDelegate, the first one you left empty will handle Universal links when the app has not been launched yet.
Implement the following methods in your SceneDelegate to handle Universal Links when the app is already running and when it is not launched yet:
//This method is called when app is NOT running in the background.
//Easy to test with fatalError()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
if let userActivity = connectionOptions.userActivities.first {
debugPrint("userActivity: \(userActivity.webpageURL)")
fatalError()
}
}
//This method is called when app is running in the background.
//Easy to test with debugPrint
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
debugPrint("userActivity: \(userActivity.webpageURL)")
}
From there, do whatever you need to handle the links.
Hope it helps :)
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.
I am trying to handle open apps from universal link click. below ios 13 its working good but for ios 13 its working only app running in background. If app not working foreground or background, clicking link opens app not called continue userActivity function. I also tried to get it in scene delegate willconnnect to delegate. But still not calling
My code is below what is wrong?
scene delegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
if connectionOptions.userActivities.first != nil {
self.scene(scene, continue: userActivity!)
}
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
continueUserActivity(userActivity: userActivity)
}
func continueUserActivity(userActivity : NSUserActivity){
if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
let url = userActivity.webpageURL!
let dataDict:[String: String] = [AppLinkManager.appLinkExtraKey: url.absoluteString]
NotificationCenter.default.post(name: .didReceiveAppLink, object: nil, userInfo: dataDict)
}
}
app delegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if let userActivityDict = launchOptions?[.userActivityDictionary] as? [AnyHashable : Any],
let userActivity = userActivityDict["UIApplicationLaunchOptionsUserActivityKey"] as? NSUserActivity {
continueUserActivity(userActivity: userActivity)
}
return true
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
continueUserActivity(userActivity: userActivity)
return true
}
func continueUserActivity(userActivity : NSUserActivity){
if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
let url = userActivity.webpageURL!
let dataDict:[String: String] = [AppLinkManager.appLinkExtraKey: url.absoluteString]
NotificationCenter.default.post(name: .didReceiveAppLink, object: nil, userInfo: dataDict)
}
}
Have you tried implementing continue userActivity function in SceneDelegate:
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { }
I faced the same issue when using Firebase DynamicLink and looks like UniversalLinks (and probably several other APIs) use this callback on the SceneDelegate.
So if you're targeting iOS 13 and below try implementing both for SceneDelegate and AppDelegate