CallKit Framework Overview
CallKit is a new framework and is available in iOS 10 and later. Used to implement VoIP functionality, call blocking, and identification in your app. Apps can use CallKit to receive incoming calls and outgoing calls using the native call UI.
VoIP Push Notification
A VoIP app lets the user make and receive phone calls using an Internet connection instead of the device’s cellular service. Because a VoIP app relies heavily on the network, it’s no surprise that making calls result in high energy use. When not in active use, however, a VoIP app should be completely idle to conserve energy.
For more information about VoIP push notification and PushKit, see Voice over Internet Protocol (VoIP) (https://developer.apple.com/library/content/documentation/Performance/Conceptual/EnergyGuide-iOS/OptimizeVoIP.html#//apple_ref/doc/uid/TP40015243-CH30) and PushKit Framework (https://developer.apple.com/reference/pushkit).
Step 1: Prepare To Receive VoIP Push Notification
Now open your application target.Choose capabilities and select ‘Voice over IP’ under background modes
Step 2: Configure VoIP Push Notification
To configure your app to receive VoIP push notifications, import to the PushKit framework in your app delegate. Create a PKPushRegistry object, set its delegate to self, and register to receive VoIP pushes.
// import to the PushKit framework in App delegate import PushKit // VoIP registration on launch func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { self.registreVoIP() return true } // Register for VoIP notifications method func registreVoIP() { let voipRegistry = PKPushRegistry(queue: DispatchQueue.main) voipRegistry.delegate = self voipRegistry.desiredPushTypes = [.voIP] }
Implement a delegate method to handle updated push credentials. If your app receives both standard push notifications and VoIP pushes, then your app will receive two separate push tokens. Both tokens must be passed to the server to receive notifications.
// Handle updated push credentials func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, forType type: PKPushType){ // Register VoIP push token (a property of PKPushCredentials) with server } // Handle incoming pushes func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, forType type: PKPushType){ // Process the received push }
Step 3: Handle Received Notification
Payload data which you will receive in didReceiveIncomingPushWith method and this method is called even in Background mode.
// Handle incoming pushes func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, forType type: PKPushType){ // Process the received push guard type == .voIP else { return } if let strUUID = payload.dictionaryPayload["UUID"] as? String, let handle = payload.dictionaryPayload["handle"] as? String, let hasVideo = payload.dictionaryPayload["hasVideo"] as? Bool, let uuid = UUID(uuidString: strUUID){ // Handle incoming call } }
Configure CallKit Framework
Create a “ProviderDelegate” class to handle incoming calls action by extending CXProviderDelegate.
import Foundation import UIKit import CallKit final class ProviderDelegate: NSObject, CXProviderDelegate { private let provider: CXProvider }
Step 1: Configure CallKit capabilities
Configure your call by representing its CallKit capabilities. A CXProviderConfiguration object controls the native call UI for incoming and outgoing calls, including a localized name for the provider, the ringtone to be played for incoming calls, and the icon to be displayed during calls. A provider configuration can also set the maximum number of call groups and a number of calls in a single call group, determine whether to use emails and/or phone numbers as handles and specify whether a video is supported.
// Provider configuration, representing its CallKit capabilities static var providerConfiguration: CXProviderConfiguration { let providerConfiguration = CXProviderConfiguration(localizedName: NSLocalizedString("APPLICATION_NAME", comment: "Name of application")) providerConfiguration.supportsVideo = true providerConfiguration.maximumCallsPerCallGroup = 1 providerConfiguration.supportedHandleTypes = [.phoneNumber] if let iconMaskImage = UIImage(named: "IconMask") { providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation(iconMaskImage) } providerConfiguration.ringtoneSound = "Ringtone.caf" return providerConfiguration }
Step 2: Initialise ProviderDelegate
Initialise CXProvider class with the type and provider configuration which we have just declared.
init() { provider = CXProvider(configuration: type(of: self).providerConfiguration) super.init() provider.setDelegate(self, queue: nil) }
Create a method named reportIncomingCall with some basic parameters to update CXCallUpdate by describing the CXHandle type and value like in type you can give options like .phoneNumber, EmailAddress and Generic and a provider will report new Incoming Call to CXProvider.
Using the information provided by the external notification, the app creates a UUID and a CXCallUpdate (https://developer.apple.com/reference/callkit/cxcallupdate) object to uniquely identify the call and the caller and passes them both to the provider using reportNewIncomingCall(with: update: completion:) (https://developer.apple.com/reference/callkit/cxprovider/1930694-reportnewincomingcall) method.
// Use CXProvider to report the incoming call to the system func reportIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((NSError?) -> Void)? = nil) { // Construct a CXCallUpdate describing the incoming call, including the caller. let update = CXCallUpdate() update.remoteHandle = CXHandle(type: .phoneNumber, value: handle) update.hasVideo = hasVideo // Report the incoming call to the system provider.reportNewIncomingCall(with: uuid, update: update) { error in /* Only add incoming call to the app's list of calls if the call was allowed (i.e. there was no error) since calls may be "denied" for various legitimate reasons. See CXErrorCodeIncomingCallError. */ completion?(error as? NSError) } }
Step 3 : Handling CXProviderDelegate
Called when the provider has been reset. Delegates must respond to this callback by cleaning up all internal call state (disconnecting communication channels, releasing network resources, etc.). This callback can be treated as a request to end all calls without the need to respond to any actions.
func providerDidReset(_ provider: CXProvider) { print("Provider did reset") /* End any ongoing calls if the provider resets, and remove them from the app's list of calls since they are no longer valid. */ }
The CXCallAction subclass is an abstract class that represents an action associated with a CXCall Object. The CallKit framework provides several concrete CXCallAction subclasses to represent actions such as answering a call and putting a call on hold. Each instance of CXAction is uniquely identified by a UUID, which is generated on initialization. An action also tracks whether it has been completed or not.
Step 4 : Accept call using CXAnswerCallAction
The Call is connected, the system sends provider(_:perform:) to the provider delegate. In your implementation, the delegate is responsible for configuring an AVAudioSession and calling fulfill() on the action when finished.
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { /* Perform your action after accepting the call Ex. 1. Configure the audio session, but do not start to call audio here, since it must be done once the audio session has been activated by the system after having its priority elevated. 2. Trigger the call to be answered via the underlying network service. 3. Signal to the system that the action has been successfully performed. action.fulfill() 4. Signal to the system that the action was unable to be performed. action.fail() */ }
Step 5 : End call using CXEndCallAction
When the user initiates an outgoing call the provider sends provider(_: perform:) to its delegate. The Provider’s delegate calls the fulfill() method to indicate that the action was successfully performed.
func provider(_ provider: CXProvider, perform action: CXEndCallAction) { /* Perform your action after ending the call Ex. 1. Stop call audio whenever ending the call. 2. Trigger the call to be ended via the underlying network service. 3. Signal to the system that the action has been successfully performed. action.fulfill() 4. Remove the ended call from the app's list of calls. */ }
Step 6 : Hold the call using CXSetHeldCallAction
When a caller places a call on hold, callers are unable to communicate with one another until the holding caller removes the call from hold. Placing a call on hold doesn’t end the call.
When the user or the system places a call on hold the provider sends provider(_: perform:) to its delegate. The Provider’s delegate calls the fulfill() method to indicate that the action was successfully performed.
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) { /* Perform your action holding or unholding the call Ex. 1. Stop or start audio in response to holding the call. 2. Signal to the system that the action has been successfully performed. action.fulfill() */ }
Step 7: Initiate call from AppDelegate
AppDelegate you can call a displayIncomingCall method inside didReceiveIncomingPushWith PKPushRegitryDelegate method to initialise your call.
class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate { var providerDelegate: ProviderDelegate? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool{ providerDelegate = ProviderDelegate(callManager: callManager) return true } // Handle incoming pushes func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, forType type: PKPushType) { // Process the received push guard type == .voIP else { return } if let uuidString = payload.dictionaryPayload["UUID"] as? String, let handle = payload.dictionaryPayload["handle"] as? String, let hasVideo = payload.dictionaryPayload["hasVideo"] as? Bool, let uuid = UUID(uuidString: uuidString) { // Handle incoming call displayIncomingCall(uuid: uuid, handle: handle, hasVideo: hasVideo) } } // Display the incoming call to the user func displayIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((NSError?) -> Void)? = nil) { print("uuid \(uuid)") providerDelegate?.reportIncomingCall(uuid: uuid, handle: handle, hasVideo: hasVideo, completion: completion) } }
Source Code:
Note
Use of VoIP and CallKit framework in your app you’ll need to give the proper description of the usage of both feature otherwise they may reject your metadata at the time of app Review.
More reference
- CallKit Framework ( https://developer.apple.com/reference/callkit )
- Enhancing VoIP Apps with CallKit ( https://developer.apple.com/videos/play/wwdc2016/230/ )