CallKit - Bandyer/Bandyer-iOS-SDK GitHub Wiki

The Kaleyra Video iOS SDK has built-in support for CallKit framework. The SDK leverages the power of CallKit framework allowing your app to seamlessly integrate with the iOS call system. The SDK will take care of interfacing with CallKit framework without you to know how it works or that it exists at all. Well, sort of... there are a few caveats that must be taken into account.

This guide is meant for the 3.0 SDK version. If you need the guide for a previous SDK version, please take a look at this guide instead.

Table of Contents:

Features

Apple's CallKit framework enables VoIP apps to integrate their calling services with other call-related apps on the system. CallKit provides the calling interface, and your app handles the back-end communication with Kaleyra Video VoIP service through the Kaleyra Video iOS SDK. For incoming and outgoing calls, CallKit displays the same interfaces as the Phone app, giving your app a more native look and feel. CallKit also responds appropriately to system-level behaviors such as Do Not Disturb. Enabling CallKit support in the Kaleyra Video iOS SDK allows your app to take advantage of the following features (only a few are listed below) provided by the CallKit framework:

  • Calls won't be interrupted when the user puts the app in background
  • Calls won't be interrupted when the user locks her / his screen
  • Your app can receive incoming calls when it is in suspended state (requires VoIP notifications)
  • Your app can receive incoming calls when it is in background state (requires VoIP notifications)
  • Calls won't be interrupted when a regular phone call is received
  • Calls won't be interrupted when a VoIP calls is received by another app installed on the user's device
  • Seamless integration with the Phone app
  • The device ringtone is played when an incoming call is received
  • Do not disturb is enforced by the system

Beware, when Kaleyra Video SDK CallKit support is disabled all of the features above won't be available to your app. Thus, if your app is put in the background by the user while a call is in progress, the call will end.

Requirements

  • CallKit framework requires you to specify some background application modes in your app configuration.
  • Applications downloaded from the Chinese AppStore MUST disable CallKit framework.
  • VoIP push notifications are required in order to fully take advantage of CallKit features.

Application Setup

The following steps will guide you through the configuration of your app to enable CallKit support in the Kaleyra Video SDK.

App background modes

In order for CallKit to work properly the app background modes must be updated to let the system know that your App supports VOIP communication. This trivial step must be done once and then you can almost forget about it, but it is extremely important otherwise CallKit is not going to work. The easiest way to enable background modes is adding those capabilities through Xcode. Otherwise you can add those capabilities in your App Info.plist file by yourself. The required background modes needed to make CallKit work are: audio and voip. You can enable them in Xcode's Capabilities panel of your App. Make sure "Background modes" switch is on and both "Audio, AirPlay and Picture in Picture" and "Voice over IP" checkboxes are enabled.

App Background Modes Xcode

Disabling CallKit for China AppStore

If you plan to release your app in the Chinese AppStore, please follow along this chapter. If you know you are not going to ship your application on the Chinese AppStore you can safely skip this chapter altogether.

Starting from May 2018, any app using the CallKit framework cannot be sold on the Chinese AppStore anymore. Chinese government has explicitly forbidden Apple from selling apps using CallKit on the Chinese AppStore. If you plan to release your app in the Chinese AppStore you must disable the Kaleyra Video iOS SDK CallKit functionality before submitting your app for review. Even if you disable the CallKit functionality in your app, you must prove to app reviewers your app does not uses CallKit when it is downloaded from the Chinese AppStore (CallKit framework is linked to your app binary anyway because the Kaleyra Video SDK links it strongly).

In order to pass the app review process and comply with the Chinese laws, you have three options:

Disable CallKit only for users having the Chinese region locale

This is the most preferred way to disable the CallKit framework only for Chinese audience. It can be easily accomplished by checking the device locale when setting up the Kaleyra Video iOS SDK and disabling the SDK CallKit functionality when the locale country region is China. The following snippets of code show you how to do it:

import UIKit
import Bandyer

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let config: Config

        if let regionCode = Locale.current.regionCode, regionCode.lowercased() == "cn" {
            config = try! ConfigBuilder(appID: "my app id", environment: .production, region: .europe)
					        .callKit { callkit in
					                   callkit.disabled()
					        }
					        .build()
        } else {
            config = try! ConfigBuilder(appID: "my app id", environment: .production, region: .europe)
					        .callKit { callkit in
					                   callkit.enabled()
					        }
					        .build()
        }

        BandyerSDK.instance.configure(config)

        return true
    }
}
#import "AppDelegate.h"
#import <Bandyer/Bandyer.h>

@interface AppDelegate ()

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    BDKConfig *config;
    if ([[NSLocale.currentLocale.countryCode lowercaseString] isEqualToString:@"cn"])
    {
	config = BDKConfigBuilder.create(@"My app id", BDKEnvironmentSandbox, BDKRegionIndia)
                                 .callKit(^(BDKCallKitConfigurationBuilder * callkit) {
                                            callkit.disabled();
                                 })
                                 .build();
    } else
    {
	config = BDKConfigBuilder.create(@"My app id", BDKEnvironmentSandbox, BDKRegionIndia)
                                 .callKit(^(BDKCallKitConfigurationBuilder * callkit) {
                                            callkit.enabled();
                                 })
                                 .build();
    }

    [BandyerSDK.instance configure:config];

    return YES;
}

@end

This solution has some drawbacks. First, let's pretend a Chinese user living in UK is using your app, if the user has set her/his device country region setting to "China mainland", CallKit functionality is going to be disabled for the user even if she/he should not be affected by the restriction enforced in China country. Second, you are going to face an App review rejection anyway, unless you inform the App reviewers telling them how your app disables CallKit when it is running on devices in China.

Make a specific app for Chinese AppStore

Even if it sounds dumb, one way to overcome this issue is two create two separate apps, one for global audience and one for the Chinese AppStore. The former will take advantage of CallKit features whereas the latter will not. The drawback of this approach is that you are now required to make two separate Apps, and well you know the drill... two AppStore connect pages, two binaries, #ifdef preprocessor flags scattered throughout your code base.

Disable CallKit for all users in any country

In order to disable CallKit for all users you must initialise the Kaleyra Video iOS SDK with CallKit disabled. This is the most restrictive solution because it disables CallKit for any user using your app, and because your app cannot take advantage of all the great features of CallKit. Take a look at the Opt-out CallKit section of this guide for an example of how to disable the SDK CallKit integration. Beware, you are going to face an App review rejection even if you disabled CallKit in your app unless you explicitly tell the app reviewers how CallKit functionality is disabled in your app when the app is running on devices in China.

App review rejection

In case your app build has been rejected by the AppStore reviewers because of CallKit, don't panic, it is likely you are going to have the possibility to reply to the reviewers explaining how you have disabled CallKit in your app for the Chinese audience. For further information take a look at this link from the Apple forums

SDK Configuration

Although CallKit support is enabled by default in the Kaleyra Video iOS SDK, you might need to customize the system call UI, to do that you should provide your custom data to the SDK Configuration object. We will show you how to do it in the code section below. Let's pretend, we are configuring the Kaleyra Video iOS SDK in the application's UIApplicationDelegate application:didFinishLaunchingWithOptions: method. The following snippet of code will help you customize the system call UI:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    let config = try! ConfigBuilder(appID: "my app id", environment: .production, region: .europe)
                                   .callKit { callkit in
					      callkit.enabled { providerBuilder in
					                        providerBuilder.supportedHandles([.generic])
					                                       .ringtoneSound("ringtone.mp3")
					                                       .icon(UIImage(named: "callkit-icon")!)
					      }
				   }
		                   .build()
        
    BandyerSDK.instance.configure(config)

    return true
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    BDKConfig *config = BDKConfigBuilder.create(@"My app id", BDKEnvironmentSandbox, BDKRegionIndia)
                                        .callKit(^(BDKCallKitConfigurationBuilder * callkit) {
                                              callkit.enabledWithConfiguration(^(BDKCallKitProviderConfigurationBuilder * provider {
                                                                provider.ringtoneSound(@"my ringtone.mp3")
                                                                        .iconImage([UIImage imageNamed:@"callkit-icon"])
                                                                        .supportedHandles(@[@(CXHandleTypePhoneNumber), @(CXHandleTypeEmailAddress)]);
                                                                });
                                        })
                                        .build();
                  
    [BandyerSDK.instance configure:config];

    return YES;
}

App Icon

When a call is in progress and the system UI is presented, the user has the ability to return to your application touching an a button on the right egde of the screen identified by the name of your App. By default the system doesn't show an icon for that button, and it looks very weird as you can see in the image below

CallKit App Icon 1

If you want to show an icon for that button, you must tell the Kaleyra Video iOS SDK which image must be shown, like so:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    let config = try! ConfigBuilder(appID: "my app id", environment: .production, region: .europe)
			           .callKit { callkit in
					      callkit.enabled { providerBuilder in
					                        providerBuilder.icon(UIImage(named: "callkit-icon")!)
					      }
		                   }
			           .build()
        
    BandyerSDK.instance.configure(config)

    return true
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    BDKConfig *config = BDKConfigBuilder.create(@"My app id", BDKEnvironmentSandbox, BDKRegionIndia)
                                        .callKit(^(BDKCallKitConfigurationBuilder * callkit) {
                                                 callkit.enabledWithConfiguration(^(BDKCallKitProviderConfigurationBuilder * provider {
                                                                provider.iconImage([UIImage imageNamed:@"callkit-icon"])
                                                 });
                                        })
                                        .build();
                                      
    [BandyerSDK.instance configure:config];

    return YES;
}

Beware you must provide a png image 40x40 points. The alpha channel of the image will be used to create a white mask for the native system UI. For more info head over to https://developer.apple.com/documentation/callkit/cxproviderconfiguration/2274376-icontemplateimagedata.

This is the final result when an icon is provided:

CallKit App Icon 2

Custom ringtone

When an incoming call is received CallKit will play the default system ringtone based on user preferences on the device. If you want to play a different ringtone anytime an incoming call is received you can do so providing the name of the file to reproduce found in your Application Main Bundle.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    let config = try! ConfigBuilder(appID: "my app id", environment: .production, region: .europe)
		                   .callKit { callkit in
					      callkit.enabled { providerBuilder in
					                        providerBuilder.ringtoneSound("ringtone.mp3")
					      }
			           }
			           .build()
        
    BandyerSDK.instance.configure(config)

    return true
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    BDKConfig *config = BDKConfigBuilder.create(@"My app id", BDKEnvironmentSandbox, BDKRegionIndia)
                                        .callKit(^(BDKCallKitConfigurationBuilder * callkit) {
                                                  callkit.enabledWithConfiguration(^(BDKCallKitProviderConfigurationBuilder * provider {
                                                                provider.ringtoneSound(@"my ringtone.mp3")
                                                  });
                                        })
                                        .build();
                                      
    [BandyerSDK.instance configure:config];

    return YES;
}

Contact Handles

Kaleyra Video SDKs are designed to work with the bare minimum information about a user, this means that user contact information like first name, last name, email and so on, are not available in the SDK. We refer to a user in the Kaleyra Video platform through hers/his "userId", which is an alphanumeric string unique within a company (you can think of it as a slug). This approach has the advantage that we don't store any user information on our back-end, nor we send user's information on the network, but it has one drawback, whenever the end user has to be presented with contact information about another user, the only information when can show her/him is an "user alias". This holds also true for CallKit. Whenever the system call UI is presented to the end-user, we must provide it an handle object that has the purpose (in CallKit land) of identifying a contact in the end-user's address book.

The following image will show how the system looks like if you don't provide an handle provider to the SDK

CallKit Handle Provider 1

If you look at the name of the caller in the image above, you'll see that instead of the caller name a weird id appears.

TL;DR

CallKit needs CXHandle objects to identify a contact inside the end-user address book. When an incoming or outgoing call is going to be processed by CallKit, the Kaleyra Video SDK must provide those CXHandle objects to it. In order to create CXHandle objects with meaningful user information, the SDK must be provided an object that is able to create CXHandle objects from "user aliases". The UserDetailsProvider protocol serves this purpose. For more information take a look at our guide. This is the final result when you implement a custom UserDetailsProvider:

CallKit handle provider 2

As you might have noticed, the weird id that was appearing as the caller name is now replaced by a meaningful contact name.

Video Button

When a call is being performed the native system call UI will show a video button that is enabled only when the call has video support, as you can see in the image below.

CallKit video button

If the user taps the button a SiriKit Intent is generated and forwarded to your App delegate. In order to enable the video in the call you must forward the received Siri intent to the Kaleyra Video SDK CallViewController.

Beware, SiriKit has changed its public api on iOS 13.0. In iOS 10.0 till iOS 13.0 you will receive an NSUserActivity containing an interaction object that carries a INStartVideoCallIntent object whereas starting from iOS 13.0 the interaction object will carry a INStartCallIntent. If your app has a minimum deployment target lower than iOS 13.0 it is likely you are going receive an INStartVideoCallIntent even on iOS 13.0 anyway.

The following code will show you how to handle those intents.

import UIKit
import Bandyer

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var callWindow: CallWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        [...]        
        return true
    }    
}

extension AppDelegate {
    func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
     //When System call ui is shown to the user, it will show a "video" button if the call supports it.
     //The code below will handle the siri intent received from the system and it will hand it to the call view controller
     //if the controller is presented

     guard let siriIntent = userActivity.interaction?.intent else {
         return false
     }

     guard let window = CallWindow.instance else {
         return false
     }

     if #available(iOS 13.0, *) {
         if let startCallIntent = siriIntent as? INStartCallIntent {
              window.handle(startCallIntent: startCallIntent)
              return true
         }
     }

     if let videoCallIntent = siriIntent as? INStartVideoCallIntent {
          window.handle(startVideoCallIntent: videoCallIntent)
          return true
     }
     return false
}

#import <Intents/Intents.h>
#import <Bandyer/Bandyer.h>

#import "AppDelegate.h"

@interface AppDelegate ()

@property (nonatomic, strong) BDKCallWindow *callWindow;

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [...]

    return YES;
}

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id <UIUserActivityRestoring>> *__nullable restorableObjects))restorationHandler
{
    //When System call ui is shown to the user, it will show a "video" button if the call supports it.
    //The code below will handle the siri intent received from the system and it will hand it to the call view controller
    //if the controller is presented

    if (@available(iOS 13.0, *))
    {
        if ([userActivity.interaction.intent isKindOfClass:INStartCallIntent.class])
        {
            [self.callWindow handleINStartCallIntent:(INStartCallIntent *) userActivity.interaction.intent];

            return YES;
        } else if ([userActivity.interaction.intent isKindOfClass:INStartVideoCallIntent.class])
        {
            [self.callWindow handleINStartVideoCallIntent:(INStartVideoCallIntent *) userActivity.interaction.intent];
            
            return YES;
        }
    } else
    {
        if ([userActivity.interaction.intent isKindOfClass:INStartVideoCallIntent.class])
        {
            [self.callWindow handleINStartVideoCallIntent:(INStartVideoCallIntent *) userActivity.interaction.intent];

            return YES;
        }
    }

    return NO;
}


@end

Opt-out CallKit

If you choose to opt-out CallKit altoghether, you can do so telling the Kaleyra Video SDK to not use CallKit during the SDK initialization.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    let config = try! ConfigBuilder(appID: "my app id", environment: .production, region: .europe)
			           .callKit { callkit in
					      callkit.disabled()
			           }
			           .build()
        
    BandyerSDK.instance.configure(config)

    return true
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    BDKConfig *config = BDKConfigBuilder.create(@"My app id", BDKEnvironmentSandbox, BDKRegionIndia)
                                        .callKit(^(BDKCallKitConfigurationBuilder * callkit) {
                                                   callkit.disabled()
                                        })
                                        .build();
                                      
    [BandyerSDK.instance configure:config];

    return YES;
}

Sample apps

We created two sample apps, one in objective-c and one in swift to show you how to integrate the Kaleyra Video SDK in your app with CallKit support enabled.

Where to go from here

Now that you have configured your app to handle CallKit you should take a look at our VoIP notifications guide which will show you how to integrate VoIP nofications enabling your app to receive calls even when the app is suspended or in background.

What's next