Opening content from URL Scheme when App is closed - ios

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.

Related

iOS, Swift 5, AppDelegate open url / .onOpenUrl - how to find calling app that calls my custom scheme

So the old way of handling openURL calls in UIKit was to use the following in an AppDelegate:
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { ...
Unfortunately the new #main SwiftUI this is redirected to
.onOpenUrl { url in ...
Thus we miss the dictionary OpenURLOptionsKey.sourceApplication which I would like to determine which app called mine, and then can make the appropriate actions as I have a use case that would require limiting certain scheme urls to be used by 3rd party apps and not others.
What's the best practice to do this?
I've not been getting anywhere with the documentation, or searching here. It looked like I might have got a lead on it via onContinueUserActivity/NSUserActivityTypeBrowsingWeb but that doesn't get called either.
So I managed to use this in the app delegate:
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
let sceneConfig = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
sceneConfig.delegateClass = SceneDelegate.self
return sceneConfig
}
Which sets this up in the scenedelegate:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
// get url and sourceApp out of first URLContext here
}
Unfortunately, if the sender of the URL is NOT from your team, you'll get nil in the UIOpenURLContext.sourceApp
So if you're wanting to create RPCs between your teams apps, you're fine.
If you want to communicate between apps from different teams, you'll have to figure out some other way to indicate which app it is and then apply your responding policy to that.

deep links in ios - app not in background

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

iOS 13+: Can't get a notification userInfo when opening a deeplink

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.

Branch.io links Facebook issue

I implemented branch.io links and it is working fine for WhatsApp and from Notes, but not Facebook Messenger or any other place with in-app browser.
As per documentation:
To work around this limitation, your links must have deepviews or something similar enabled, with a call-to-action link/button that has a Universal Link behind it. This way, clicking a link from the app feed will open a webview containing your deepview page, and the user can then click the link/button to launch your app.
For example Facebook Messenger should open in-app browser with deepview of my link, and if user tap button - my app should be opened, it is fine for me, but doesn't work like this.
In my case facebook is opening in-app browser with my deepview and automatically (I didn't tap button) redirects me to... AppStore :(. But if I open my app manually, I'm getting callback and app is redirecting me to proper place.
It is recommended to use forced redirections for links shared on Facebook since Universal Links work conditionally. You can force Facebook to open your app by appending $uri_redirect_mode=2 as a query parameter to your Branch link.
eg: https://example.app.link/83jlowd0?$uri_redirect_mode=2
You can learn more about forced redirections here- https://docs.branch.io/pages/links/integrate/#forced-redirections
If you still face issues, please write to support#branch.io and someone from the Branch team will be able to assist you with this.
I had the same problem. the branch object returned from Branch.io SDK was returning an object without all parameters.
All I did was follow the great document provided by Branch.io
document provided by Branch.io
So as I was using the Scene in my app. Calling these methods in my sceneDelegate solved the issue.
import UIKit
import Branch
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
guard let _ = (scene as? UIWindowScene) else { return }
// workaround for SceneDelegate continueUserActivity not getting called on cold start
if let userActivity = connectionOptions.userActivities.first {
BranchScene.shared().scene(scene, continue: userActivity)
}
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
BranchScene.shared().scene(scene, continue: userActivity)
}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
BranchScene.shared().scene(scene, openURLContexts: URLContexts)
}
}

iOS Callkit - incoming Call Recents History

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

Resources