CallKit Tutorial for iOS
Learn how your app can use CallKit for system-level phone integration and how to build a directory extension for call blocking and identification. By Andrew Kharchyshyn.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
CallKit Tutorial for iOS
30 mins
- Getting Started
- What is CallKit?
- CXProvider
- CXCallController
- Incoming Calls
- ProviderDelegate
- CXProviderDelegate
- Test Flight
- Ending the Call
- CXProviderDelegate
- Requesting Transactions
- Other Provider Actions
- Handling Outgoing Calls
- Starting the Call
- Managing Multiple Calls
- Creating a Call Directory Extension
- Settings Setup
- Where to Go From Here?
Other Provider Actions
The documentation page of CXProviderDelegate shows that there are many more actions that the provider can perform, including muting and grouping or setting calls on hold. The latter sounds like a good feature for Hotline. Why not implement it now?
When the user wants to set the held status of a call, the app will send an instance of CXSetHeldCallAction
to the provider. It’s your job to implement the related delegate method. Open ProviderDelegate.swift and add the following implementation to the class extension:
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
action.fail()
return
}
// 1.
call.state = action.isOnHold ? .held : .active
// 2.
if call.state == .held {
stopAudio()
} else {
startAudio()
}
// 3.
action.fulfill()
}
This code does the following:
- After getting the reference to the call, update its status according to the
isOnHold
property of the action. - Depending on the status, start or stop processing the call’s audio.
- Mark the action fulfilled.
Since this is also a user-initiated action, you’ll need to expand the CallManager
class as well. Open CallManager.swift and add the following implementation just below end(call:)
:
func setHeld(call: Call, onHold: Bool) {
let setHeldCallAction = CXSetHeldCallAction(call: call.uuid, onHold: onHold)
let transaction = CXTransaction()
transaction.addAction(setHeldCallAction)
requestTransaction(transaction)
}
The code is very similar to end(call:)
. In fact, the only difference between the two is that this one will wrap an instance of CXSetHeldCallAction
into the transaction. The action will contain the call’s UUID and the held status.
Now it’s time to connect this action to the UI. Open CallsViewController.swift and find the class extension marked with UITableViewDelegate
at the end of the file. Add the following implementation to the class extension, just below tableView(_:titleForDeleteConfirmationButtonForRowAt:)
:
override func tableView(
_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath
) {
let call = callManager.calls[indexPath.row]
call.state = call.state == .held ? .active : .held
callManager.setHeld(call: call, onHold: call.state == .held)
tableView.reloadData()
}
When the user taps a row, the code above will update the held status of the corresponding call.
Build and run the application and start a new incoming call. If you tap the call’s cell, you’ll notice that the status label will change from Active to On Hold.
Handling Outgoing Calls
The final user-initiated action you’ll implement is making outgoing calls. Open ProviderDelegate.swift and add the following implementation to the CXProviderDelegate
class extension:
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
let call = Call(uuid: action.callUUID, outgoing: true,
handle: action.handle.value)
// 1.
configureAudioSession()
// 2.
call.connectedStateChanged = { [weak self, weak call] in
guard
let self = self,
let call = call
else {
return
}
if call.connectedState == .pending {
self.provider.reportOutgoingCall(with: call.uuid, startedConnectingAt: nil)
} else if call.connectedState == .complete {
self.provider.reportOutgoingCall(with: call.uuid, connectedAt: nil)
}
}
// 3.
call.start { [weak self, weak call] success in
guard
let self = self,
let call = call
else {
return
}
if success {
action.fulfill()
self.callManager.add(call: call)
} else {
action.fail()
}
}
}
The provider will invoke this delegate method when an outgoing call request is made:
- After creating a
Call
with the call’s UUID from the call manager, you’ll have to configure the app’s audio session. Just as with incoming calls, your responsibility at this point is only configuration. The actual processing will start later, whenprovider(_:didActivate)
is invoked. - The delegate monitors the call’s lifecycle. It’ll initially report that the outgoing call has started connecting. When the call is connected, the provider delegate will report that as well.
- Calling
start()
on the call triggers its lifecycle changes. Upon a successful connection, the call can be marked as fulfilled.
Starting the Call
Now that the provider delegate is ready to handle outgoing calls, it’s time to teach the app how to make one. Open CallManager.swift and add the following method to the class:
func startCall(handle: String, videoEnabled: Bool) {
// 1
let handle = CXHandle(type: .phoneNumber, value: handle)
// 2
let startCallAction = CXStartCallAction(call: UUID(), handle: handle)
// 3
startCallAction.isVideo = videoEnabled
let transaction = CXTransaction(action: startCallAction)
requestTransaction(transaction)
}
This method will wrap a Start call action into a CXTransaction
and request it from the system.
- A handle, represented by
CXHandle
, can specify the handle type and its value. Hotline supports phone number handles, so you’ll use it here as well. - A
CXStartCallAction
receives a unique UUID and a handle as input. - Specify whether the call is audio-only or a video call by setting the
isVideo
property of the action.
It’s time to hook up the new action to the UI. Open CallsViewController.swift and replace the previous implementation of unwindForNewCall(_:)
with the following:
guard
let newCallController = segue.source as? NewCallViewController,
let handle = newCallController.handle
else {
return
}
let videoEnabled = newCallController.videoEnabled
let incoming = newCallController.incoming
if incoming {
let backgroundTaskIdentifier =
UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
AppDelegate.shared.displayIncomingCall(
uuid: UUID(),
handle: handle,
hasVideo: videoEnabled
) { _ in
UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
}
}
} else {
callManager.startCall(handle: handle, videoEnabled: videoEnabled)
}
There’s one subtle change in the code: When incoming
is false, the view controller will ask the call manager to start an outgoing call.
That’s all you’ll need to make calls. It’s time to start testing! Build and run the app on your device. Tap the plus button in the right-hand corner to start a new call, but this time make sure that you select Outgoing from the segmented control.
At this point, you should see the new call appear in your list. You’ll also see different status labels based on the current stage of the call:
Managing Multiple Calls
What if a Hotline user receives multiple calls? You can simulate this by placing first an outgoing and then an incoming call and pressing the Home button before the incoming call comes in. At this point, the app presents the user with the following screen:
The system lets the user decide how to resolve the issue. Based on the user’s choice, it will combine multiple actions into a CXTransaction
. For example, if the user chooses to end the ongoing call and answer the new one, the system will create a CXEndCallAction
for the former and a CXStartCallAction
for the latter. Both actions will be wrapped into a transaction and sent to the provider, which will process them individually. If your app already knows how to fulfill the individual requests, your work is done!
You can test it by resolving the scenario above. The list of calls will reflect your choice. The app will only process one audio session at a time. If you choose to resume a call, the other will be put on hold automatically.