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.
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 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)
}
}
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.
performActionFor shortcutItem gets called in my SceneDelegate if the app has already launched, but it doesn't get called if the app actually launches from a shortcut item. Why is this?
You can get the ShortcutItems from willConnectTo function in sceneDelegate when the app is launched from shortcut item (and when there is no instance of app is in background)
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let shortcutItems = connectionOptions.shortcutItem{
}
}
I've implemented call kit in our app only for received incoming call when the app is close or on background (push call notification). I just noticed that every time I received a call and use callkit to display it, this call automatically appear in the call history (Recents tab in native Call App).
Every time I click on one of those recent, my App is resume or launch.
I wanted to make the app place an outgoing call after the user press the recent call but I didn't find anything about it.
Is there a way to detect that the app was opened / resumed from this call recent click ?
Can we disable this callkit feature ?
Thanks for providing information :)
I wanted to make the app place an outgoing call after the user press
the recent call but I didn't find anything about it.
In your app's Info.plist, you must have INStartAudioCallIntent and/or INStartVideoCallIntent in the NSUserActivityTypes key, and your app delegate must implement the -application:continueUserActivity:restorationHandler: method to handle the start call intent. See the Speakerbox example app for details.
Can we disable this callkit feature ?
If you don't set a remoteHandle for the call's CXCallUpdate, the item in Recents won't be pressable.
for future reference;
call kit provider configuration should have this list of generic and phone number types
config.supportedHandleTypes = [.generic,.phoneNumber]
Callkit update remotehandle should be initialized like below
update.remoteHandle = CXHandle(type: .generic, value:String(describing: payload.dictionaryPayload["caller_id"]!))
3.You should add Intents.framework to your project by selecting project>target>Build Phases> Link Binary With Libraries and click + button
You should add INStartCallIntent to your info.plist like below
<key> NSUserActivityTypes </key>
<array>
<string>INStartCallIntent</string>
</array>
for swift 5 you should add below function to your SceneDelegate.swift
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {}
or for swift 4 and below you should add below function to Appdelegate.swift
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
return true
}
then add code below to your continue useractivity function
let interaction = userActivity.interaction
if let startAudioCallIntent = interaction?.intent as? INStartAudioCallIntent{
let contact = startAudioCallIntent.contacts?.first
let contactHandle = contact?.personHandle
if let phoneNumber = contactHandle?.value {
print(phoneNumber)
// Your Call Logic
}
}
}
you should get a warning that
INStartAudioCallIntent' was deprecated in iOS 13.0: INStartAudioCallIntent is deprecated. Please adopt INStartCallIntent instead
applying this suggestion fails because startAudioCallIntent cant be cast to INStartCallIntent so ignore it.
VERY IMPORTANT continue useractivity function in scene delegate is not called whan app is terminated so to run your intent when app is start you should add code block to
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {}
and your code should be like below
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(AppDelegate.shared)) // This is my code so you may not use Appadelegate.shared. ignore it
self.window = window
window.makeKeyAndVisible()
}
if let userActivity = connectionOptions.userActivities.first {
let interaction = userActivity.interaction
if let startAudioCallIntent = interaction?.intent as? INStartAudioCallIntent{
let contact = startAudioCallIntent.contacts?.first
let contactHandle = contact?.personHandle
if let phoneNumber = contactHandle?.value {
// Your Call Logic
}
}
}
}