-
Notifications
You must be signed in to change notification settings - Fork 0
CallKit
The Bandyer 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.
- Requirements
- Application Setup
- SDK Configuration
- Opt-out CallKit
- Sample apps
- Where to go from here
- What's next
- CallKit framework has been introduced in iOS 10.0, so it can be used from iOS 10.0 onward.
- Requires you to specify some background application modes in your app configuration.
The following steps will guide you through the configuration of your app to enable CallKit support in the BandyerSDK.
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.
CallKit framework is not a required SDK dependency (it is a weak dependency), so your app must link CallKit if you want to enable the support for it. In order for the SDK to be aware of it, you must link your app against CallKit.framework. To do so, open up your App project in Xcode and select your App .xcodeproj file in the "Project Navigator" panel.
Then, under the "General" tab, move to "Linked frameworks and Libraries" and select the "plus" icon. In the dialog panel that has open up, select CallKit.framework and click on the "Add" button.
Now CallKit.framework should be listed in the "Linked Frameworks and Libraries" list.
Although linking the CallKit.framework should be sufficient to enable support in the 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 Bandyer SDK in the UIApplicationDelegate
's application:didFinishLaunchingWithOptions:
method. The following snippet of code will help you customize the system call UI:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
BDKConfig *config = [BDKConfig new];
config.environment = BDKEnvironment.production;
//On iOS 10 and above this statement is not needed, the default configuration object
//enables CallKit by default, it is here for completeness sake
config.callKitEnabled = YES;
//The following statement is going to change the name of the app that is going to be shown by the system call UI.
//If you don't set this value during the configuration, the SDK will look for to the value of the
//CFBundleDisplayName key (or the CFBundleName, if the former is not available) found in your App Info.plist
config.nativeUILocalizedName = @"My wonderful app";
//The following statement is going to change the ringtone used by the system call UI when an incoming call
//is received. You should provide the name of the sound resource in the app bundle that is going to be used as
//ringtone. If you don't set this value, the SDK will use the default system ringtone.
config.nativeUIRingToneSound = @"MyRingtoneSound";
//The following statements are going to change the app icon shown in the system call UI. When the user answers
//a call from the lock screen or when the app is not in foreground and a call is in progress, the system
//presents the system call UI to the end user. One of the buttons gives the user the ability to get back into your
//app. The following statements allows you to change that icon.
//Beware, the configuration object property expects the image as an NSData object. You must provide a side
//length 40 points square png image.
//It is highly recommended to set this property, otherwise a "question mark" icon placeholder is used instead.
UIImage *callKitIconImage = [UIImage imageNamed:@"callkit_icon"];
config.nativeUITemplateIconImageData = UIImagePNGRepresentation(callKitIconImage);
//The following statement is going to tell CallKit the app supports email address as handles for contacts
//For a complete list of handle types see https://developer.apple.com/documentation/callkit/cxhandletype?language=objc
config.supportedHandleTypes = [NSSet setWithObject:@(CXHandleTypeEmailAddress)];
//The following statement is going to tell the BandyerSDK which handle provider must use to translate caller or callee user alias into CXHandle objects
//CXHandle objects are used by CallKit to show user information in the system UI when a call is in progress
config.handleProvider = [MyHandleProvider new];
[BandyerSDK.instance initializeWithApplicationId:APP_ID config:config];
return YES;
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let config = BDKConfig()
config.environment = .production
//On iOS 10 and above this statement is not needed, the default configuration object
//enables CallKit by default, it is here for completeness sake
config.isCallKitEnabled = true
//The following statement is going to change the name of the app that is going to be shown by the system call UI.
//If you don't set this value during the configuration, the SDK will look for to the value of the
//CFBundleDisplayName key (or the CFBundleName, if the former is not available) found in your App Info.plist
config.nativeUILocalizedName = "My wonderful app"
//The following statement is going to change the ringtone used by the system call UI when an incoming call
//is received. You should provide the name of the sound resource in the app bundle that is going to be used as
//ringtone. If you don't set this value, the SDK will use the default system ringtone.
config.nativeUIRingToneSound = @"MyRingtoneSound";
//The following statements are going to change the app icon shown in the system call UI. When the user answers
//a call from the lock screen or when the app is not in foreground and a call is in progress, the system
//presents the system call UI to the end user. One of the buttons gives the user the ability to get back into your
//app. The following statements allows you to change that icon.
//Beware, the configuration object property expects the image as an NSData object. You must provide a side
//length 40 points square png image.
//It is highly recommended to set this property, otherwise a "question mark" icon placeholder is used instead.
let callKitIcon = UIImage(named: "callkit-icon")
config.nativeUITemplateIconImageData = callKitIcon?.pngData()
//The following statement is going to tell CallKit the app supports email address as handles for contacts
//For a complete list of handle types see https://developer.apple.com/documentation/callkit/cxhandle/handletype
let handleTypes: Set<NSNumber> = [NSNumber(value: CXHandle.HandleType.emailAddress.rawValue)]
config.supportedHandleTypes = handleTypes
//The following statement is going to tell the BandyerSDK which handle provider must use to translate caller or callee user alias into CXHandle objects
//CXHandle objects are used by CallKit to show user information in the system UI when a call is in progress
config.handleProvider = MyHandleProvider()
BandyerSDK.instance().initialize(withApplicationId: "YOUR_APP_ID", config: config)
return true
}
In order to show a localized application name in the system UI, you should provide a value to the "nativeUILocalizedName" property of the BandyerSDK config object. For example if you provide "My Wonderful App" to that property like in the following code:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[...]
config.nativeUILocalizedName = @"My Wonderful App";
[...]
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
[...]
config.nativeUILocalizedName = "My Wonderful App"
[...]
}
When an incoming call or an outgoing call is in progress the system UI will be presented like this:
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
If you want to show an icon for that button, you must tell the BandyerSDK which image must be shown, like so:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[...]
UIImage *callKitIconImage = [UIImage imageNamed:@"callkit-icon"];
config.nativeUITemplateIconImageData = UIImagePNGRepresentation(callKitIconImage);
[...]
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
[...]
let callKitIcon = UIImage(named: "callkit-icon")
config.nativeUITemplateIconImageData = callKitIcon?.pngData()
[...]
}
Beware you must provide the raw png data of a 40 points square image. 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:
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.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[...]
config.nativeUIRingToneSound = @"MyRingtoneSound";
[...]
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
[...]
config.nativeUIRingToneSound = "MyRingtoneSound"
[...]
}
Bandyer 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 Bandyer platform through hers/his "user alias", 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
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.
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 Bandyer 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 BCXHandleProvider protocol serves this purpose. You should create or provide an instance of an object conforming to this protocol when the SDK is being configured. Here we are going to show you how to configure the sdk and how a possible implementation of the BCXHandleProvider looks like.
//The application delegate method where we are going to config the sdk
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
BDKEnvironment *env = BDKEnvironment.production;
BDKConfig *config = [BDKConfig new];
config.environment = env;
//Previous configuration code [...]
//This statement is needed to inform CallKit which kind of CXHandle types it should expect
config.supportedHandleTypes = [NSSet setWithArray:@[@(CXHandleTypeEmailAddress), @(CXHandleTypeGeneric)]]
//Here we are pretending an HandleProvider class exists and conforms to BCXHandleProvider protocol. The instance provided to the SDK will be used when the system call UI must be shown to the end-user.
config.handleProvider = [[HandleProvider alloc] init];
//Then we initialize the SDK as shown in the "Getting Started" guide.
[BandyerSDK.instance initializeWithApplicationId:APP_ID config:config];
return YES;
}
//Here we create a dummy object showing you how an HandleProvider can be implemented
@interface HandleProvider : NSObject <BCXHandleProvider>
//Let's pretend this object allow us to access some kind of storage service
@property (nonatomic, strong) id storage;
@end
@implementation HandleProvider
- (void)handleForAliases:(nullable NSArray<NSString *> *)aliases
completion:(nonnull void (^)(CXHandle *_Nonnull))completion
{
CXHandle *handle = nil;
NSMutableArray *contacts = [NSMutableArray array];
for (NSString *alias in aliases)
{
//Here we fetch the contact object identified by "alias" from the storage service
Contact *c = [self.storage fetchContact:alias];
[contacts addObject:c];
}
NSMutableArray *handles = [NSMutableArray arrayWithCapacity:contacts.count];
for (Contact *contact in contacts)
{
//Here we store every contact's fullname in an array for later use.
//We are assuming that every contact has a fullname and any nil value won't be returned by the fullName method
[handles addObject:contact.fullName];
}
//We create a generic handle merging all contacts full name separated by a comma
handle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:[contacts componentsJoinedByString:@", "]];
//Then we complete the task calling the completion block
completion(handle);
}
@end
Beware, the
handleForAliases:completion:
method is called on a background queue, you should take that into account if you need to access resources that have threading concerns.
This is the final result when you implement a custom handle provider:
As you might have noticed, the weird id that was appearing as the caller name is now replaced by a meaningful contact name.
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.
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 BandyerSDK call view controller.
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
The following code will show you how to handle those intents.
#import <Intents/Intents.h>
#import <Bandyer/Bandyer.h>
#import "AppDelegate.h"
@interface AppDelegate ()
@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
{
if (@available(iOS 13.0, *))
{
if ([userActivity.interaction.intent isKindOfClass:INStartCallIntent.class])
{
//It is up to you to get the Bandyer view controller
BDKCallViewController *callViewController = [self getTheCallViewController];
[callViewController handleINStartCallIntent:(INStartCallIntent *) userActivity.interaction.intent];
return YES;
}
} else
{
if (@available(iOS 10.0, *))
{
if ([userActivity.interaction.intent isKindOfClass:INStartVideoCallIntent.class])
{
//It is up to you to get the Bandyer view controller
BDKCallViewController *callViewController = [self getTheCallViewController];
[callViewController handleINStartVideoCallIntent:(INStartVideoCallIntent *) userActivity.interaction.intent];
return YES;
}
}
}
return NO;
}
@end
import UIKit
import Bandyer
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
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 {
if #available(iOS 13.0, *){
if userActivity.interaction?.intent is INStartCallIntent{
let callController = getCallViewController()
callController.handle(startCallIntent: userActivity.interaction?.intent as! INStartCallIntent)
return true
}
} else {
if #available(iOS 10.0, *){
if userActivity.interaction?.intent is INStartVideoCallIntent{
let callController = getCallViewController()
callController.handle(startVideoCallIntent: userActivity.interaction?.intent as! INStartVideoCallIntent)
return true
}
}
}
return false
}
}
If you choose to opt-out CallKit altoghether, you can do so telling the BandyerSDK to not use CallKit during the SDK initialization. The "callKitEnabled" property on BDKConfig class serves this purpose.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
config.callKitEnabled = NO;
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
config.isCallKitEnabled = false
}
If your app is targeting iOS 9.0 CallKit support is not available. CallKit framework is available from iOS 10.0.
We created two sample apps, one in objective-c and one in swift to show you how to integrate the BandyerSDK in your app with CallKit support enabled.
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.
Looking for other platforms? Take a look at Android, Flutter, ReactNative, Ionic / Cordova. Anything unclear or inaccurate? Please let us know by submitting an Issue or write us here.