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.

Leave a rating/review
Download materials
Save for later
Share
Update note: Andrew Kharchyshyn updated this tutorial for Xcode 10, Swift 4.2 and iOS 12. József Vesza wrote the original.

Life on iOS wasn’t always perfect for VoIP (Voice over IP) app developers. In particular, delivering notifications was tough. With your app in the background, a user’s only option was a regular notification, which is easy to miss. Without a rich, built-in call UI, your app wouldn’t feel very integrated.

Fortunately, Apple introduced CallKit in iOS 10!

In this tutorial you’ll get a glimpse of CallKit’s power by building an app that:

  • Uses system services to report incoming and outgoing calls.
  • Manages a call directory to identify or block incoming calls.
Note: CallKit features won’t work in the simulator. To follow along with this tutorial, you’ll need an iPhone with iOS 12.0 or higher installed.

Getting Started

Download the starter project using the Download Materials button at the top or bottom of this tutorial. Then, set up code signing to debug the project on your device. Open the project file in Xcode and select Hotline in the Project navigator.

Start by changing the bundle identifier. With the project selected, go to the General tab and find the Identity section. Change the bundle identifier to something unique:

Changing the bundle identifier

Next, look for the Signing section. Select your preferred development team (in my case, it’s my personal team) in the drop-down next to Team. Be sure to check Automatically manage signing. This allows Xcode to automatically create the provisioning profile for the app.

Setting up code signing

Note: If you see an Add Account… button, you’ll need to enter your Apple Developer account credentials before being able to select a development team.

To test your setup, build and run the app on your iPhone.

This app does NOTHING!

This app does NOTHING!

This app does NOTHING!

Currently the app won’t do much, but you’ll notice that there are several source files in the starter project. Those files are mostly responsible for setting up the UI and handling user interactions. There are two main classes worth a look before moving on:

  • Call represents a phone call. The class exposes properties for identifying calls (such as its UUID, or handle) as well as lifecycle callbacks indicating when the user starts, answers or ends a call.
  • CallManager currently maintains the list of ongoing calls in the app and has methods for adding or removing calls. You’ll expand this class further throughout the tutorial.

What is CallKit?

CallKit is a framework that aims to improve the VoIP experience by allowing apps to integrate with the native phone UI. By adopting CallKit, your app will be able to:

  • Use the native incoming call screen in both the locked and unlocked states.
  • Start calls from the native phone app’s Contacts, Favorites and Recents screens.
  • Interplay with other calls in the system.

In this section, you’ll get more familiar with the CallKit architecture. The diagram below shows all the key players:

CallKit Architecture

When working with CallKit, there are two primary classes you’ll interact with: CXProvider and CXCallController. Time to dive in!

CXProvider

Your app will use CXProvider to report any out-of-band notifications to the system. These are usually external events, such as an incoming call.

When such an event occurs, CXProvider creates a call update to notify the system. Call updates encapsulate new or changed call-related information. They are of the CXCallUpdate class, which exposes properties such as the caller’s name or whether a call is video or audio-only.

When the system wants to notify the app of events, it usesCXAction instances. CXAction is an abstract class representing telephony actions. For each action, CallKit provides a different concrete implementation of CXAction. For instance, CXStartCallAction represents initiating an outgoing call, while CXAnswerCallAction answers an incoming call. A unique UUID identifies each action, which can fail or fulfill.

Apps can communicate with CXProvider through the CXProviderDelegate protocol, which defines methods for provider lifecycle events and incoming actions.

CXCallController

The app will use CXCallController to inform the system of user-initiated requests, such as a Start call action. This is the key difference between the CXProvider and the CXCallController: The provider reports to the system whereas the call controller makes requests from the system on behalf of the user.

The call controller uses transactions to make these requests. Transactions, represented by CXTransaction, contain one or more CXAction instances. The call controller sends transactions to the system. If everything is in order, the system responds with the appropriate action to the provider.

What does this look like in practice?

Incoming Calls

The diagram below shows a high-level overview of an incoming call flow:

incoming

  1. In response to an incoming call, the app constructs a CXCallUpdate and uses the provider to send it to the system.
  2. The system publishes this as an incoming call to all of its services.
  3. When the user answers the call, the system sends a CXAnswerCallAction instance to the provider.
  4. The app answers the call by implementing the appropriate CXProviderDelegate method.

ProviderDelegate

First, create the delegate for the provider. Head back to Xcode, and with the App group highlighted in the Project navigator, create a new file called ProviderDelegate.swift.

Add the following code to the file:

import AVFoundation
import CallKit

class ProviderDelegate: NSObject {
  // 1.
  private let callManager: CallManager
  private let provider: CXProvider
  
  init(callManager: CallManager) {
    self.callManager = callManager
    // 2.
    provider = CXProvider(configuration: ProviderDelegate.providerConfiguration)
    
    super.init()
    // 3.
    provider.setDelegate(self, queue: nil)
  }
  
  // 4.
  static var providerConfiguration: CXProviderConfiguration = {
    let providerConfiguration = CXProviderConfiguration(localizedName: "Hotline")
    
    providerConfiguration.supportsVideo = true
    providerConfiguration.maximumCallsPerCallGroup = 1
    providerConfiguration.supportedHandleTypes = [.phoneNumber]
    
    return providerConfiguration
  }()
}

This is what’s happening in the code above:

  1. Store references to both the provider and the call controller. The provider delegate will interact with both of them.
  2. Initialize the provider with the appropriate CXProviderConfiguration, stored as a static variable below. A provider configuration specifies the behavior and capabilities of the calls.
  3. Set the delegate to respond to events coming from the provider. This line will cause a build error, as ProviderDelegate doesn’t conform to CXProviderDelegate yet.
  4. In the case of Hotline, the provider configuration allows video calls and phone number handles and restricts the number of call groups to one. For further customization, refer to the CallKit documentation.

Just below the configuration, add the following helper method:

func reportIncomingCall(
  uuid: UUID, 
  handle: String, 
  hasVideo: Bool = false,
  completion: ((Error?) -> Void)?
) {
  // 1.
  let update = CXCallUpdate()
  update.remoteHandle = CXHandle(type: .phoneNumber, value: handle)
  update.hasVideo = hasVideo
  
  // 2.
  provider.reportNewIncomingCall(with: uuid, update: update) { error in
    if error == nil {
      // 3.
      let call = Call(uuid: uuid, handle: handle)
      self.callManager.add(call: call)
    }
    
    // 4.
    completion?(error)
  }
}

This helper method allows the app to call the CXProvider API to report an incoming call. Here’s what’s going on:

  1. Prepare a call update for the system which will contain the relevant call metadata.
  2. Invoke reportNewIncomingCall(with:update:completion) on the provider to notify the system of the incoming call.
  3. The completion handler will be called once the system processes the call. Assuming no errors, you create a Call instance and add it to the list of calls via the CallManager.
  4. Invoke the completion handler if it’s not nil.

Other classes in the app can invoke this method in order to simulate incoming calls.

Andrew Kharchyshyn

Contributors

Andrew Kharchyshyn

Author

József Vesza

Author

Alex Figueroa

Tech Editor

Nicole Hardina

Editor

Aleksandra Kizevska

Illustrator

Richard Turton

Final Pass Editor

Richard Critz

Team Lead

Over 300 content creators. Join our team.