How to build a VoIP app with iOS CallKit and Sendbird Calls
This tutorial explains how to develop VoIP apps using Sendbird Calls and Apple’s iOS CallKit framework. By the end of this tutorial, you will understand how to:
- Configure CallKit
- Design the CallKit UI
- Execute CallKit actions
- Create a call manager
- Handle CallKit events
- Enable interaction with a custom UI
Each section provides the entire code of the file; you can copy and paste the code into the appropriate files. Remember that the provided code may not be the only implementation; you can customize the code to suit your needs.
Please note that you will gain the most benefit from this tutorial if you have a working knowledge of Swift.
That said, let’s get started! 💻
Step 1. Create a Sendbird account
- Sign up for a free Sendbird account.
- Create an account using your email or select ‘Continue with Google’.
- Set up your organization by filling out the fields ‘Organization name’ and ‘Phone number’.
- Create a new “Chat + Calls” application in the region closest to your locale.
- On the left side of the screen, you should see a ‘Calls’ menu. Go into the ‘Studio’ and create a new user. Follow the prompts on your screen.
Step 2: Configuring CallKit
Now that we’ve created a Sendbird account and added users, let’s talk about how to configure CallKit for our purposes.
To develop a VoIP app service, you need a VoIP certificate for the app. Go to the Apple Developer page and sign in.
Go to Certificate, Identifiers & Profiles > Certificates > Create a New Certificate. You will find the VoIP Services Certificate under the Services section. Create the VoIP service certificate. (Here’s how to do this.)
Go to Target > Signing & Capabilities. Add Background Modes and enable Voice over IP. This will create a .entitlements file and appropriate permissions that allow you to use VoIP services. If you don’t enable Voice over IP, a CallKit error code 1 will occur.
Step 3: Designing the CallKit UI
To configure localized information for CallKit, create a file named CXProviderConfiguration.extension.swift.
CXProvider is an object that represents a telephony provider. CXProvider is initialized with CXProviderConfiguration. VoIP apps should only create one instance of CXProvider per app and use it globally. For more information, see the official Apple docs.
Each provider can specify an object conforming to the CXProviderDelegate protocol to respond to events such as starting a call, putting a call on hold, or activating a provider’s audio session.
A CXProviderConfiguration object controls the native call UI for incoming and outgoing calls, including the localized name of the provider, ringtone to be played for incoming calls, and the icon to be displayed during calls. For more information, see the official Apple docs.
- Initialize the CXProviderConfiguration object with a localized name. This name will appear in the call view when your users receive a call through CallKit. Use appropriate naming, such as your app service name, as the localized name. In this case, we used ‘Homing Pigeon’.
- Configure the user interface and its capabilities. In this step, set iconTemplateImageData, supportsVideo, and supportedHandleTypes. If you want to further customize CallKit, see the table below. You can also refer to the official Apple docs.
|ringtoneSound||The name of the sound resource in the app bundle to be used for the provider ringtone.|
|iconTemplateImageData||The PNG data for the icon image to be displayed for the provider.|
|maximumCallGroups||The maximum number of call groups||2|
|maximumCallsPerCallGroup||The maximum number of calls per call group.||5|
|supportedHandleTypes||The supported handle types.||[ ]|
|supportsVideo||A Boolean value that indicates whether the provider supports video in addition to audio.||false|
This is a Boolean value that indicates whether the call supports video capability in addition to audio. By default, it’s set to false. See the Apple docs for more information.
If your service provides video calls, set supportsVideo to true. If your service does not provide video calls, skip this setting.
This is the type of call provider that you want to handle. See Apple’s docs about CXHandle.HandleType.
CXHandle refers to how your users are identified in each call. Three possible types of handles are: .phoneNumber, .email, and .generic. Depending on the service you provide and how you manage your users, you may choose different options. If the users are identified by their phone number or their email address, choose .phoneNumber or .email. However, if it’s based on some random UUID value or other unspecified value, use .generic, which is an unspecified String value that can be used more flexibly.
This is the PNG data for the icon image to be displayed for the provider.
According to the docs, “The icon image should be a square with a side length of 40 points. The alpha channel of the image is used to create a white image mask, which is used in the system’s native in-call UI for the button which takes the user from this system UI to the 3rd-party app.”
Set .iconTemplateImageData to the icon image that will be displayed next to the localized name on the CallKit screen. Assign .pngData() to your app icon.
Step 4. Requesting CallKit actions
We have configured CallKit and designed the UI. Now let’s understand more about CallKit actions and how to implement them.
CallKit provides many call-related features such as dialing, ending, muting, holding, etc. Each of these features should be executed by appropriate CallKit actions called CXCallAction. These actions are called from a CXCallController object, which uses CXTransaction objects to execute each CXCallAction. In order to control CallKit, you must create corresponding CXCallActions and execute them by requesting a transaction with CXTransaction.
There are three steps to send a request to CallKit:
- Create CXCallAction object
- Create CXTransaction object
- Request the CXTransaction object via CXCallController
|CXCallAction||Telephony actions, such as start call, end call, mute call, hold call, associated with a call object.||Developer – CXCallAction|
|CXTransaction||An object that contains zero or more action objects to be performed by a call controller.||Developer – CXTransaction|
|CXCallController||An object interacts with calls by performing actions and observing calls.||Developer – CXCallController|
Add CXCallController property and another method named requestTransaction(with:completionHandler:). The method creates CXTransaction with CXCallAction and requests the transaction via callController. You always have to call this method after creating a CXCallAction object.
4.2. Call actions
Start call action
The following implements a method for CXStartCallAction. This action represents the start of a call. If the action was requested successfully, a corresponding CXProviderDelegate.provider(_:perform:) event will be called.
1. You have to create a CXHandle object associated with the call that will be used to identify the users involved with the call. This object will be included in CXStartCallAction along with a UUID.
2. If the call has video, set .isVideo to true.
3. As mentioned above, don’t forget to call the requestTransaction(with:completionHandler:) method after creating a CXStartCallAction object.
End call action
The following implements another method for CXEndCallAction. This action represents that the call was ended. If the action was requested successfully, a corresponding CXProviderDelegate.provider(_:perform:) event will be called. CXEndCallAction only requires the UUID of the call. Create a CXEndCallAction object with the UUID.
Other call actions
Other CXCallActions can be implemented the same as CXStartCallAction and CXEndCallAction. Here is the list of other call actions:
|Call Action||Description||Corresponding Event|
|CXAnswerCallAction||Answers an incoming call.||CXProviderDelegate.provider(_:perform:)|
|CXSetHeldCallAction||Places a call on hold or removes a call from hold.||CXProviderDelegate.provider(_:perform:)|
|CXSetMutedCallAction||Mutes or unmutes a call.||CXProviderDelegate.provider(_:perform:)|
|CXSetGroupCallAction||Mutes or unmutes a call.||CXProviderDelegate.provider(_:perform:)|
Step 5. Managing calls
To easily manage CXCallController and call IDs, you may want to create a call manager which must be accessible from anywhere. The call manager will store and manage UUIDs of the ongoing calls to handle call events.
NOTE: You can also use the CXCallController.callObserver.calls property that manages a list of active calls (including ended calls) and observes call changes. Each call is a CXCall object that represents a call in CallKit. By checking the hasEnded attribute, you can handle ongoing calls.
Create a new class named CallManager. Then, add a shared static instance to access it from everywhere (You may choose to use patterns other than singleton).
If you want to know more about this pattern, see the Apple docs about managing a shared resource using a singleton.
Add callIDs property with a type of [UUID] and add methods for managing callIDs: addCall(uuid:), removeCall(uuid:) and removeAllCalls().
Step 6. Handling CallKit events
To report new incoming calls or respond to new CallKit actions, you have to create a CXProvider object with the CXProviderConfiguration that was created in Section 3. You can also handle CallKit events of the call via CXProviderDelegate.
1. Import CallKit and create a ProviderDelegate class with NSObject and CXProviderDelegate conformance.
2. Add two properties: callManager and provider. The callManager is the CallManager class that you created in Section 5. The provider reports actions for CallKit. When you initialize a provider, use the CXProviderConfiguration.custom that you already created.
3. To report a new incoming call, you need to create a CXCallUpdate instance with the relevant information about the incoming call, as well as the CXHandle that identifies the users involved in the call.
4. To make your calls richer, you can customize the CXHandle and CXCallUpdate instances. If the call has video, set hasVideo to true. The upper iPhone call log is based on the CXHandle object.
5. After reporting a new incoming call, you have to add it to CallManager.shared.calls by using the addCall(uuid:) method that was added earlier.
6. CallKit keeps track of the connected time of the call and the end time of the call by listening to appropriate CallKit events. To tell the CallKit that the call was connected, call reportOutgoingCall(with:connectedAt:). This initiates the call duration elapsing, and informs the starting point of the call that is displayed in the call log of the iPhone app.
To tell the CallKit that the call was ended, call reportCall(with:endedAt:reason:). This informs the end point of the call that will be displayed in the call log of the iPhone app as well.
Step 7. Handle CXCallAction event
Interaction with CallKit UI
When the provider performs CXCallActions, corresponding CXProviderDelegate methods can be called. In order to properly respond to the users’ actions, you have to implement appropriate Sendbird Calls actions in the method.
Important: Don’t forget to execute action.fulfill() before the method is ended
Note: To access the UUID of the call, you have to use action.callUUID property, not action.uuid.
For more information about CXProviderDelegate methods, refer to the official Apple docs.
Step 8. Interaction with UI
You can start and end calls with CallKit using its default view. Next, let’s try to use a custom UI with CallKit. For the sake of clarity, this tutorial skips creating related storyboard files and ViewController files. Instead, suppose that there is one text field for entering the remote user’s ID, one button for making an outgoing call, another button for receiving an incoming call, and the last button for ending the call.
This is what your code will then look like:
- Make an outgoing call: Because the user is initiating a call, you have to create a request for the call. This action requires the callee’s user ID and unique UUID of the call.
- Implement the action for the end button: This action will end the call based on the callID.
- Answer an incoming audio call: To do this, you have to simulate an incoming audio call. Because CallKit is not aware of the incoming call, you have to report to CallKit about the incoming call. This action requires the caller’s user ID and unique UUID of the call. Currently, because the incoming call is made locally, you will use a randomly generated UUID() instead of a real call’s UUID. If you want to test incoming video calls, assign the value of hasVideo parameter as true.
And that’s it! You know how to build a VoIP app using Sendbird Calls and the Apple CallKit framework. In this tutorial, you learned how to configure CallKit, design a UI, as well as manage and handle CallKit actions and events. You are on your way to building awesome, engaging apps with voice and video calls. Happy iOS calling app building – we can’t wait to see what you build! 😎