iOS Callkit - incoming Call Recents History - ios

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

Related

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.

Swift Firebase RemoteConfig: How to fetchAndActivate in background

I have a new feature on my iOS app that is disabled by default. To manage this, I use Firebase RemoteConfig to do this, and added new_feature_enabled = false to a config_debug.plist file. I then set this as the default using remoteConfig.setDefaults(fromPlist: "config_debug").
My app has a background task, which when upon receiving a silent push notification, it would wake up the iOS app in the background to perform some tasks.
The issue I am facing is, after I enable the new feature remotely on Firebase console (ie setting new_feature_enabled = true on the console), and trigger the silent push notification, the app wakes up but the feature is reverted to disabled mode. I then kill the app, and launched the app again, the feature then got enabled.
In other words, every time the device receives the silent push notification and wakes up in the background, the remoteConfig resets to the local config.plistfile instead of using the remote version set on Firebase console.
Code:
//In AppDelegate
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
//handle silent PN and do background tasks...
handleSilentPN(userInfo: userInfo)
completionHandler(.newData)
}
func handleSilentPN(userInfo: [AnyHashable: Any]) {
guard let scheduler = userInfo["scheduler"] as? [String: AnyObject] else {return}
guard let rootViewController = (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController as? MainTabController else {
return
}
let vc = SomeController()
let controller = UINavigationController(rootViewController: vc)
rootViewController.present(controller, animated: false)
}
//At SceneDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
//Setup remote config
setupRemoteConfig()
let vc = MainTabController()
setupWindow(viewController: vc, scene: scene)
}
fileprivate func setupWindow(viewController: UIViewController, scene: UIScene) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
window?.windowScene = windowScene
window?.rootViewController = viewController
}
func setupRemoteConfig() {
let remoteConfig = RemoteConfig.remoteConfig()
//Throttles for development to see changes frequently
#if DEBUG
let settings = RemoteConfigSettings()
settings.minimumFetchInterval = 0
remoteConfig.configSettings = settings
#endif
//Define a default setting
#if DEBUG
remoteConfig.setDefaults(fromPlist: "config_debug")
#else
remoteConfig.setDefaults(fromPlist: "config_release")
#endif
remoteConfig.fetchAndActivate { (status, error) in
if let error = error {
Log("Err setingup remote config: \(error.localizedDescription)")
}
//0: .successFetchedFromRemote
//1: .successUsingPreFetchedData
//2: .error
Log("Remote config status: \(status.rawValue)")
}
}
My code syncs RemoteConfig at SceneDelegate and ensures they are the remote version that is fetched from Firebase console. I suspected that setupRemoteConfig was called before didReceiveRemoteNotification and therefore did not sync the remote version.
I reattempted by moving setupRemoteConfig to didFinishLaunchingWithOptions and didReceiveRemoteNotification but both seems to give the same results.
I then tested which functions get called when the app wakes up in the background, and noticed that the following do not get called:
didFinishLaunchingWithOptions
application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions)
scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)
sceneDidBecomeActive(_ scene: UIScene)
Where should I call setupRemoteConfig() so that I can maintain the remote version of the remoteConfig after receiving silent PN, and ensure that the new feature is enabled?

Universal Links callback function not called in SceneDelegate

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

Opening content from URL Scheme when App is closed

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.

Using UIScenes in iOS 13, how do I AirPlay Mirror a screen (seems to default to external display)

If I compile onto an iOS 12 device (doesn't use UIScene) and AirPlay Mirror to my Apple TV the app is mirrored as expected to the TV.
On an iOS 13 device, it seems to treat it as an external display where it's formatted to fit the screen (but I have no way to control it).
I'd prefer the old functionality of just mirroring it.
How do I accomplish mirroring on iOS 13? I'm digging around in the docs for:
application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration
And in the UISceneConfiguration there's a role property (it has UISceneSession.Role.windowExternalDisplay when I try to AirPlay Mirror) but it doesn't seem to have any value like UISceneSession.Role.windowMirror.
I've been playing around with mirroring and external displays and various possibilities exist with just the right combination of code/settings but certain functionality doesn't seem possible.
Under iOS 13 (with an app built with a Base SDK of iOS 13), you can get your app to be mirrored on an external display. But making this work prevents your app from showing different content on an external display. Basically your app only mirrors or it only shows a unique scene for an external display.
If you wish to only have your app be mirrored, then ensure the following:
Remove the application(_:configurationForConnecting:options:) from your App Delegate.
In the Info.plist, make sure there is no entry for the "External Display Session Role" under the "Scene Configuration" section of the "Application Scene Manifest".
If neither of those two things are part of your app then your app will simple mirror to any external screen when you activate Screen Mirroring on the iOS device.
Just ran into this issue myself. My solution actually came from within my UIWindowSceneDelegate class.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
// External displays should not get assigned a window. When a window isn't assigned, the default behavior is mirroring.
guard session.role != .windowExternalDisplay else { return }
/* the rest of your setup */
}
When you don't assign a window, it seems that mirroring becomes the default option. Before that change, my external displays (screen mirroring) were given their own unique UIWindow instance.
I don't see this documented anywhere, and it is not intuitive. Because of this, I'm somewhat fearful that it will break in the future.
Hope it still helps.
I found that with Objective-C implementation, you can achieve the screen mirroring behavior by returning nil in application:configurationForConnectingSceneSession:options:.
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {
if (connectingSceneSession.role == UIWindowSceneSessionRoleExternalDisplay) {
return nil;
}
UISceneConfiguration *configuration = [[UISceneConfiguration alloc] initWithName:#"Main" sessionRole:connectingSceneSession.role];
configuration.storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
configuration.delegateClass = [SceneDelegate class];
configuration.sceneClass = [UIWindowScene class];
return configuration;
}
Be aware that this is not a documented way and may break in the future.
Edited:
In Swift, you can achieve this via method swizzling:
#UIApplicationMain
class AppDelegate : UIResponder, UIApplicationDelegate {
override init() {
_ = AppDelegate.performSceneConfigurationSwizzle
super.init()
}
private static let performSceneConfigurationSwizzle: Void = {
method_exchangeImplementations(
class_getInstanceMethod(AppDelegate.self, #selector(AppDelegate.application(_:configurationForConnecting:options:)))!,
class_getInstanceMethod(AppDelegate.self, #selector(AppDelegate.swizzle_application(_:configurationForConnecting:options:)))!
)
}()
#objc func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
fatalError("Should never reach.")
}
#objc private func swizzle_application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration? {
if connectingSceneSession.role == .windowExternalDisplay {
return nil
}
// build scene configuration as usual…
}
}
Instead of implementing the AppDelegate scene configuration method in iOS 13:
#available(iOS 13.0, *)
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
let configuration = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
configuration.delegateClass = SceneDelegate.self
return configuration
}
I instead switched to using the Info.plist variant (and removed the above code) where you effectively specify all the above in your Info.plist instead. (For an up to date version of what's expected in the Info.plist file, simply create a New Project in Xcode and copy the contents from the new Info.plist file for the Application Scene Manifest key).
It now works perfectly and AirPlay Mirror mirrors as expected. I did try changing the role to windowApplication as iOS seemingly does with the Info.plist variant but it still doesn't work.

Resources