Imagine your app seamlessly integrated with the native iOS calling experience - your users can receive and make calls through your application while enjoying the same features they're accustomed to with regular phone calls. That's the power of CallKit, Apple's framework designed specifically for integrating VoIP services into iOS apps.
In today's digital landscape, communication is key, and voice calling remains a crucial feature for many applications. Whether you're building a business communication tool, a social networking app, or a customer service platform, integrating high-quality calling features can significantly enhance your user experience.
This comprehensive guide will walk you through everything you need to know about CallKit - from fundamental concepts to advanced implementation techniques.
What is CallKit and Why Use It?
Understanding CallKit: The iOS VoIP Integration Framework
CallKit is a framework introduced by Apple in iOS 10 that allows VoIP (Voice over Internet Protocol) apps to integrate with the native iOS calling interface. Before CallKit, VoIP apps were forced to use custom interfaces and couldn't fully integrate with iOS system features, creating a fragmented user experience.
The key benefits of using CallKit include:
- Native User Experience: CallKit allows your app to provide the same calling interface as the built-in Phone app, including the full-screen incoming call UI and in-call controls.
- System Integration: Your app's calls can interact with system features like Do Not Disturb, Call Waiting, and Recents list.
- Enhanced Call Management: Users can easily switch between calls, put calls on hold, or merge calls - just like with regular phone calls.
- Caller ID Functionality: Your app can display caller information, improving the identification experience for users.
Compared to implementing a custom UI for calls, CallKit provides a more seamless, familiar experience that users already understand and expect. This leads to higher user satisfaction and reduces the learning curve for your app.
CallKit Essentials: Key Concepts and Components
Core CallKit Concepts: Providers, Actions, and Transactions
To effectively implement CallKit, you need to understand its core components:
- CXProvider: Represents your VoIP service. It communicates with the system about calls and receives user interactions. You configure it with your branding and capabilities.
- CXCallController: Used to request changes to calls (like starting, ending, or putting calls on hold).
- CXTransaction: A set of actions to be performed together. Transactions ensure that multiple call-related actions happen atomically.
- CXAction: Represents a specific call-related action, such as starting a call, answering a call, or putting a call on hold.
- CXCallUpdate: Contains information about a call's state or configuration, like caller ID or whether the call supports video.
- CXHandle: Represents the identity of a call participant, typically a phone number or email address.
These components work together in a structured way:
- Your app configures a
CXProvider
to represent your VoIP service - For user-initiated actions (like making a call), you create appropriate
CXAction
objects - You bundle these actions into a
CXTransaction
- You submit the transaction using a
CXCallController
- For incoming calls, you report them to the system via the
CXProvider
- You implement a delegate for the
CXProvider
to handle system events and user interactions
Setting up Your CallKit Project
Initializing CallKit: Setting Up Your iOS Project for VoIP
Before diving into implementation, you need to set up your project properly:
- Add the required frameworks:
- CallKit
- PushKit (for VoIP push notifications)
- Enable background modes:
- In your project's capabilities, enable "Voice over IP" background mode
- Enable "Remote notifications" background mode
- Set up push notification entitlements:
- Add the Push Notifications capability to your app
- Request necessary permissions:
- Request notification permissions from the user
Here's a basic setup example:
1import CallKit
2import PushKit
3
4// Example: Requesting Push Notification Authorization
5UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
6 if let error = error {
7 print("Error requesting authorization: \(error)")
8 }
9}
10
11// Setting up PushKit for VoIP notifications
12let voipRegistry = PKPushRegistry(queue: nil)
13voipRegistry.delegate = self // Your class should conform to PKPushRegistryDelegate
14voipRegistry.desiredPushTypes = [.voIP]
15
Basic Call Handling: Making and Receiving Calls
Implementing Basic Calls: Initiating and Answering with CallKit
Initiating Outgoing Calls
To start an outgoing call:
- Create a
CXHandle
for the recipient - Create a
CXStartCallAction
- Bundle it in a
CXTransaction
- Submit the transaction using your
CXCallController
1// Example: Starting a call
2func startCall(to recipient: String) {
3 let handle = CXHandle(type: .phoneNumber, value: recipient)
4 let callUUID = UUID()
5
6 // Save this UUID so we can reference this call later
7 self.activeCall = callUUID
8
9 let startCallAction = CXStartCallAction(call: callUUID, handle: handle)
10 startCallAction.isVideo = false // Audio-only call
11
12 let transaction = CXTransaction(action: startCallAction)
13
14 callController.request(transaction) { error in
15 if let error = error {
16 print("Error starting call: \(error)")
17 } else {
18 // Call was started successfully
19 // Now set up your VoIP session
20 }
21 }
22}
23
Handling Incoming Calls
For incoming calls, the process involves:
- Receiving a VoIP push notification via PushKit
- Creating a
CXCallUpdate
with information about the caller - Reporting the call to your
CXProvider
- Handling the user's response in your provider delegate
1// In your PKPushRegistryDelegate implementation
2func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
3 // Extract caller information from the push payload
4 let callerName = payload.dictionaryPayload["caller_name"] as? String ?? "Unknown"
5 let callerNumber = payload.dictionaryPayload["caller_number"] as? String ?? ""
6
7 // Create a UUID for this call
8 let callUUID = UUID()
9
10 // Report the incoming call
11 let update = CXCallUpdate()
12 update.remoteHandle = CXHandle(type: .phoneNumber, value: callerNumber)
13 update.localizedCallerName = callerName
14 update.hasVideo = false
15
16 provider.reportNewIncomingCall(with: callUUID, update: update) { error in
17 if let error = error {
18 print("Error reporting incoming call: \(error)")
19 } else {
20 // Save this UUID to track the call
21 self.activeCall = callUUID
22
23 // Begin preparing your app's VoIP session
24 }
25
26 // Complete the push notification handling
27 completion()
28 }
29}
30
Implementing CallKit Provider Delegate
Configuring the Provider Delegate: Handling CallKit Events
The
CXProviderDelegate
is essential for handling CallKit events and user interactions. Here's how to implement the key delegate methods:1class CallKitProviderDelegate: NSObject, CXProviderDelegate {
2 let callManager: CallManager // Your VoIP call management class
3
4 init(callManager: CallManager) {
5 self.callManager = callManager
6 super.init()
7 }
8
9 // Called when the provider has been reset
10 func providerDidReset(_ provider: CXProvider) {
11 // End all calls and reset call state
12 callManager.endAllCalls()
13 }
14
15 // Called when the system starts a call
16 func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
17 // Configure audio session
18 configureAudioSession()
19
20 // Start your VoIP call
21 callManager.startCall(action.callUUID, handle: action.handle.value)
22
23 // Notify the system that the action was successful
24 action.fulfill()
25 }
26
27 // Called when the user answers a call
28 func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
29 // Configure audio session
30 configureAudioSession()
31
32 // Answer the VoIP call
33 callManager.answerCall(action.callUUID)
34
35 // Notify the system that the action was successful
36 action.fulfill()
37 }
38
39 // Called when the user ends a call
40 func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
41 // End the VoIP call
42 callManager.endCall(action.callUUID)
43
44 // Notify the system that the action was successful
45 action.fulfill()
46 }
47
48 // Called when the audio session is activated
49 func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
50 // Start audio
51 callManager.startAudio()
52 }
53
54 // Called when the audio session is deactivated
55 func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
56 // Stop audio
57 callManager.stopAudio()
58 }
59
60 // Helper method
61 private func configureAudioSession() {
62 let audioSession = AVAudioSession.sharedInstance()
63 try? audioSession.setCategory(.playAndRecord, mode: .voiceChat)
64 try? audioSession.setActive(true)
65 }
66}
67
Advanced Call Management: Hold, Mute, DTMF
Enhancing Call Control: Hold, Mute, and DTMF Functionality
CallKit supports several advanced call features that users expect from a modern calling app:
Putting Calls on Hold
1func holdCall(_ callUUID: UUID, onHold: Bool) {
2 let setHeldAction = CXSetHeldCallAction(call: callUUID, onHold: onHold)
3 let transaction = CXTransaction(action: setHeldAction)
4
5 callController.request(transaction) { error in
6 if let error = error {
7 print("Error \(onHold ? "holding" : "unholding") call: \(error)")
8 }
9 }
10}
11
In your provider delegate, implement the corresponding method:
1func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
2 // Update your VoIP call's hold state
3 callManager.setHold(action.callUUID, onHold: action.isOnHold)
4
5 // Notify the system that the action was successful
6 action.fulfill()
7}
8
Muting Calls
1func muteCall(_ callUUID: UUID, muted: Bool) {
2 let setMutedAction = CXSetMutedCallAction(call: callUUID, muted: muted)
3 let transaction = CXTransaction(action: setMutedAction)
4
5 callController.request(transaction) { error in
6 if let error = error {
7 print("Error \(muted ? "muting" : "unmuting") call: \(error)")
8 }
9 }
10}
11
In your provider delegate:
1func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
2 // Update your VoIP call's mute state
3 callManager.setMuted(action.callUUID, muted: action.isMuted)
4
5 // Notify the system that the action was successful
6 action.fulfill()
7}
8
Sending DTMF Tones
1func sendDTMF(_ callUUID: UUID, digits: String) {
2 let playDTMFAction = CXPlayDTMFCallAction(call: callUUID, digits: digits, type: .singleTone)
3 let transaction = CXTransaction(action: playDTMFAction)
4
5 callController.request(transaction) { error in
6 if let error = error {
7 print("Error sending DTMF: \(error)")
8 }
9 }
10}
11
In your provider delegate:
1func provider(_ provider: CXProvider, perform action: CXPlayDTMFCallAction) {
2 // Send DTMF tones through your VoIP service
3 callManager.sendDTMF(action.callUUID, digits: action.digits)
4
5 // Notify the system that the action was successful
6 action.fulfill()
7}
8
Caller ID and Call Blocking
Leveraging Call Directories: Implementing Caller ID and Call Blocking
iOS allows VoIP apps to provide caller identification information and block unwanted calls using Call Directory extensions. This is particularly useful for apps that want to:
- Identify callers even if they're not in the user's contacts
- Block known spam or unwanted callers
To implement a Call Directory extension:
- Add a new target to your app of type "Call Directory Extension"
- Implement the
CXCallDirectoryProvider
methods
1class CallDirectoryHandler: CXCallDirectoryProvider {
2 override func beginRequest(with context: CXCallDirectoryExtensionContext) {
3 // Add identification entries
4 addIdentificationEntries(context: context)
5
6 // Add blocking entries
7 addBlockingEntries(context: context)
8
9 // Signal completion
10 context.completeRequest()
11 }
12
13 private func addIdentificationEntries(context: CXCallDirectoryExtensionContext) {
14 // Phone numbers should be provided in ascending order
15 // Here you would typically load these from a database or API
16
17 let phoneNumbers: [CXCallDirectoryPhoneNumber] = [
18 1234567890,
19 1234567891,
20 1234567892
21 ]
22
23 let labels = [
24 "Business Name 1",
25 "Business Name 2",
26 "Business Name 3"
27 ]
28
29 for (phoneNumber, label) in zip(phoneNumbers, labels) {
30 context.addIdentificationEntry(withNextSequentialPhoneNumber: phoneNumber, label: label)
31 }
32 }
33
34 private func addBlockingEntries(context: CXCallDirectoryExtensionContext) {
35 // Phone numbers to block, in ascending order
36 let blockedPhoneNumbers: [CXCallDirectoryPhoneNumber] = [
37 9876543210,
38 9876543211
39 ]
40
41 for phoneNumber in blockedPhoneNumbers {
42 context.addBlockingEntry(withNextSequentialPhoneNumber: phoneNumber)
43 }
44 }
45}
46
To update your Call Directory data after installation, use the
CXCallDirectoryManager
:1func updateCallDirectory() {
2 CXCallDirectoryManager.sharedInstance.reloadExtension(withIdentifier: "com.yourapp.CallDirectory") { error in
3 if let error = error {
4 print("Error reloading call directory: \(error)")
5 } else {
6 print("Call directory reloaded successfully")
7 }
8 }
9}
10
Troubleshooting Common CallKit Issues
Debugging CallKit: Addressing Common Implementation Challenges
When implementing CallKit, you might encounter several common issues:
1. VoIP Push Notifications Not Being Delivered
- Possible causes: Incorrect certificate, expired certificate, missing background modes
- Solutions:
- Verify you're using a VoIP Services certificate, not a regular push certificate
- Check that the certificate is still valid
- Ensure "Voice over IP" background mode is enabled
- Verify your app is registered for VoIP push notifications via PKPushRegistry
2. CallKit UI Not Appearing
- Possible causes: Incorrect provider configuration, errors in reporting calls
- Solutions:
- Check your CXProviderConfiguration setup
- Ensure you're calling reportNewIncomingCall correctly
- Check for errors in the completion handler of reportNewIncomingCall
3. Audio Issues
- Possible causes: Incorrect audio session configuration, timing issues
- Solutions:
- Make sure you're configuring the audio session correctly in the provider delegate
- Only activate the audio session after the provider delegate's didActivate method is called
- Use the correct audio session category (.playAndRecord) and mode (.voiceChat)
4. Delegate Methods Not Being Called
- Possible causes: Delegate not set, delegate not retained
- Solutions:
- Verify you've set the delegate property on your CXProvider
- Make sure your delegate object isn't being deallocated (retain it strongly)
CallKit Best Practices
Optimizing CallKit: Following Best Practices for Robust Integrations
To ensure a high-quality CallKit implementation:
- Handle errors gracefully: Always check for errors in completion handlers and provide fallback behavior.
- Test thoroughly: Test your implementation on different iOS versions and device models. Pay special attention to how your app behaves when:
- Multiple calls are active
- The device is locked
- The app is in the background
- The network connection is poor or interrupted
- Keep the UI responsive: Perform intensive operations asynchronously to avoid blocking the UI.
- Follow Apple's guidelines: Stay up-to-date with Apple's guidelines and best practices for CallKit.
- Provide clear branding: Configure your CXProvider with appropriate branding (app name, ringtone, icon) so users know which app is handling the call.
- Use consistent call UUIDs: Ensure you're using the same UUID for a call throughout its lifecycle.
FAQs
CallKit FAQs: Answering Your Questions About iOS VoIP Integration
Is CallKit mandatory for VoIP apps?
No, CallKit is not mandatory. You can still create a VoIP app with a custom UI. However, using CallKit provides a better user experience and is highly recommended.
Does CallKit work with other VoIP frameworks?
Yes, CallKit is designed to work with any VoIP implementation. Popular VoIP SDKs like Twilio, Agora, and Sendbird provide CallKit integration support.
What are the privacy implications of using CallKit?
CallKit adds your app's calls to the device's Recents list and makes caller information available to the system. Make sure your privacy policy covers this data sharing.
Can I use CallKit to make regular phone calls?
No, CallKit is for VoIP calls only. You cannot use it to initiate or receive regular cellular calls.
Does CallKit work in all regions?
CallKit is available worldwide but may be restricted in some regions due to local regulations. For example, CallKit functionality is limited in China.
Conclusion
CallKit represents a significant advancement in how VoIP apps can integrate with iOS. By providing a native calling experience, it elevates the quality and usability of third-party communication apps.
Implementing CallKit may require some initial effort, but the benefits are substantial: improved user experience, better system integration, and enhanced call management capabilities. As voice communication continues to be a crucial feature for many apps, mastering CallKit is a valuable skill for iOS developers.
Whether you're building the next big communication platform or simply adding calling features to your existing app, CallKit provides the tools you need to create a seamless, professional calling experience that your users will appreciate.
Want to level-up your learning? Subscribe now
Subscribe to our newsletter for more tech based insights
FAQ