iPhoneiOSCallKitPushKitSwift

CallKit iOS Swift Tutorial for VoIP Apps (Super Easy)

Table of Contents

Introduction

If you're making a video chat or audio chat app, you can give your users a much better experience by using iOS CallKit. By the end of this Swift tutorial you'll learn how to make it so that when a user tries to start a video chat or audio chat with a friend, the native iOS Call Screen will show up. You'll even learn how to customize the ringtone, add your logo to the call UI, and modify a couple other features. Here's what you'll be able to make your app do with a few lines of code. If you'd rather make your app using JavaScript instead of Swift, check out our iOS CallKit/Android ConnectionService plugin for Cordova.

callkit-custom
callkit

When one of your users sends a video chat or audio chat request to a friend, you can make this screen show up. You can download all of the code used in this tutorial with git clone https://github.com/WebsiteBeaver/callkit-tutorial.git

Note that you need a device to test out CallKit. CallKit won't work on your simulator. Also CallKit only works on iOS 10 and above.

Create New Xcode Project

Start by creating an Xcode Project and select Single View App

new_xcode_project

Name it whatever you like. This next step is very important, since CallKit won't work without it. You'll get an error that says The operation couldn’t be completed. (com.apple.CallKit.error.requesttransaction error 1.). Open up Info.plist and add Required background modes with type Array and value App provides Voice over IP services.

infoplist

Receive a Call

Open ViewController.swift and replace that code with:

import UIKit
import CallKit

class ViewController: UIViewController, CXProviderDelegate {

    override func viewDidLoad() {
        let provider = CXProvider(configuration: CXProviderConfiguration(localizedName: "My App"))
        provider.setDelegate(self, queue: nil)
        let update = CXCallUpdate()
        update.remoteHandle = CXHandle(type: .generic, value: "Pete Za")
        provider.reportNewIncomingCall(with: UUID(), update: update, completion: { error in })
    }

    func providerDidReset(_ provider: CXProvider) {
    }

    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        action.fulfill()
    }

    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        action.fulfill()
    }

}

Plugin your iPhone and press run. You'll see the incoming call screen as soon as the app loads. That's all there is to it. Can't believe CallKit is that easy? Let's go over some important details from this short piece of code.

  1. import CallKit in order to take advantage of this awesome framework
  2. CXProviderDelegate is the delegate that allows you to use CallKit. The delegate is set using setDelegate.
  3. Apple recommends that you declare an instance of CXProvider as a global variable, which makes sense because you'll need to access it several times in your code. In this case we're just making it local to simplify the code.
  4. Each user is represented by a CXHandle. It's important to note that this should be unique. No two users should have the same handle.
  5. Each call is represented by a UUID. When you call UUID() it creates a random id that represents a call. No two calls should have the same UUID. You can store this in a database and use it later on in your code if you want, but it's not necessary.
  6. reportNewIncomingCall is what actually displays in the incoming call screen.
  7. providerDidReset is required because of CXProviderDelegate
  8. You can either press reject or accept once you receive the incoming call. If you reject the call or hangup, func provider(_ provider: CXProvider, perform action: CXEndCallAction) gets called. If you accept the call func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) gets called. At this point you can start the video or audio chat.
receive-call
receive-call-answer

Send a Call

Go ahead and replace the code in ViewController.swift with

import UIKit
import CallKit

class ViewController: UIViewController, CXProviderDelegate {

    override func viewDidLoad() {
        let provider = CXProvider(configuration: CXProviderConfiguration(localizedName: "My App"))
        provider.setDelegate(self, queue: nil)
        let controller = CXCallController()
        let transaction = CXTransaction(action: CXStartCallAction(call: UUID(), handle: CXHandle(type: .generic, value: "Pete Za")))
        controller.request(transaction, completion: { error in })
    }

    func providerDidReset(_ provider: CXProvider) {
    }

}

Run this code and press the home button on your iPhone. You'll notice the green bar at the top which indicates that you're in or trying to connect to a call. If you press the lock button on your phone, you'll see that the outgoing call is trying to connect. However you actually need to tell the app when the call is connected. If you're using WebRTC for a video chat, you can tell CallKit that the call is connected once both friends can see each other in the video chat. The next example explains how to connect a call, which you need to do after you make a call.

green-bar
send-call

Connect a Call

You only need to connect a call if you send a call. If you receive a call, the call gets connected as soon as soon as you press accept. Once the call is connected, a timer will display on the call UI letting you know how long the call is lasting for. In the receive call example, the timer starts as soon as you press accept because that's when the call is connected. In the send call example, the timer does not start until you let it know when to start. You should do this as soon as both friends can talk and/or see each other. In this example, we'll pretend that it takes exactly 5 seconds for the friends to connect. You can easily use WebRTC to let CallKit know when the call is connected.

Replace the code in ViewController.swift with the following, and then press run. Once the app starts you should press the power button. Notice how the timer hasn't started yet? That's because the call is not connected. Wait 5 seconds and you'll see the timer start which means that the call is connected.

import UIKit
import CallKit

class ViewController: UIViewController, CXProviderDelegate {

    override func viewDidLoad() {
        let provider = CXProvider(configuration: CXProviderConfiguration(localizedName: "My App"))
        provider.setDelegate(self, queue: nil)
        let controller = CXCallController()
        let transaction = CXTransaction(action: CXStartCallAction(call: UUID(), handle: CXHandle(type: .generic, value: "Pete Za")))
        controller.request(transaction, completion: { error in })

        DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 5) {
            provider.reportOutgoingCall(with: controller.callObserver.calls[0].uuid, connectedAt: nil)
        }
    }

    func providerDidReset(_ provider: CXProvider) {
    }

}
send-call
connect-call

Customize CallKit

CallKit makes it easy to change ringtone, icon, call type (audio/video), whether or not the call appears in recents, and a few other features.

import UIKit
import CallKit

class ViewController: UIViewController, CXProviderDelegate {

    override func viewDidLoad() {
        let config = CXProviderConfiguration(localizedName: "My App")
        config.iconTemplateImageData = UIImagePNGRepresentation(UIImage(named: "pizza")!)
        config.ringtoneSound = "ringtone.caf"
        config.includesCallsInRecents = false;
        config.supportsVideo = true;
        let provider = CXProvider(configuration: config)
        provider.setDelegate(self, queue: nil)
        let update = CXCallUpdate()
        update.remoteHandle = CXHandle(type: .generic, value: "Pete Za")
        update.hasVideo = true
        provider.reportNewIncomingCall(with: UUID(), update: update, completion: { error in })
    }

    func providerDidReset(_ provider: CXProvider) {
    }

    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        action.fulfill()
    }

    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        action.fulfill()
    }

}

When you import your custom ringtone, make sure to Add to targets and add it to Copy Bundle Resources as shown below.

add_ringtone

copy_bundle_resources

Your custom icon should be a 120x120 png file with transparency. Drag the file into the 3x box.

custom_icon

Run this code and when you go into Recents on your phone, you won't see the call in your history. The calls from the previous sections will be in history. The ability to control whether or not calls appear in Recents only works on iOS 11 and above.

callkit-customize
include-in-recents

Receive Call When App is Closed

Imagine the following scenario. Friend 1 opens up the app and presses call Friend 2. The problem is that Friend 2 force closed your video chat app a while ago. So the question is how can you trigger the CallKit UI when Friend 2 has his or her app closed. The way to implement this is with PushKit.

Replace the code in ViewController.swift with

import UIKit
import CallKit
import PushKit

class ViewController: UIViewController, CXProviderDelegate, PKPushRegistryDelegate {

    override func viewDidLoad() {
        let registry = PKPushRegistry(queue: nil)
        registry.delegate = self
        registry.desiredPushTypes = [PKPushType.voIP]
    }

    func providerDidReset(_ provider: CXProvider) {
    }

    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        action.fulfill()
    }

    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        action.fulfill()
    }

    func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
        print(pushCredentials.token.map { String(format: "%02.2hhx", $0) }.joined())
    }

    func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
        let config = CXProviderConfiguration(localizedName: "My App")
        config.iconTemplateImageData = UIImagePNGRepresentation(UIImage(named: "pizza")!)
        config.ringtoneSound = "ringtone.caf"
        config.includesCallsInRecents = false;
        config.supportsVideo = true;
        let provider = CXProvider(configuration: config)
        provider.setDelegate(self, queue: nil)
        let update = CXCallUpdate()
        update.remoteHandle = CXHandle(type: .generic, value: "Pete Za")
        update.hasVideo = true
        provider.reportNewIncomingCall(with: UUID(), update: update, completion: { error in })
    }

}

Go to the Apple Developer website, and login. Create an App ID, and make sure to specify the Bundle ID and check Push Notifications.

create_app_id

Create a new certificate and select VoIP Services Certificate.

add_certificate

Select your app and continue until you get to a screen the says Generate Your Certificate.

generate_certificate

Open up Keychain Access and click Keychain Access followed by Certificate Assistant and then Request a Certificate From a Certificate Authority. Choose Saved to disk and save the .certSigningRequest file.

key_chain

Go back to the Apple Developer site and click Choose File, and choose the .certSigningRequest from the previous step. Click continue and download the .cer file.

Double click on the .cer file.

p12_file

Then right click on VoIP Services: com.dmarcus.CallKitTutorial. Yours will be different because you'll have a different Bundle ID. Click Export and save it as a .p12 file. Be sure to add a password.

Convert the .p12 file to a .pem file with openssl pkcs12 -in Certificates.p12 -out Certificates.pem

Go back to your Xcode project and turn on Push Notifications under Capabilities. push_notifications

Run the Xcode project on your iPhone and take a look at the bottom of Xcode

device_id

Copy the Device Id which is 6a25342c9d1941c73b37c5c28f6788efe0f7a8e681ce440f15a8eb61e679cb63 in this case.

Finally paste this into Terminal, but replace 1234 with the password you set for you .pem file. Also make sure Certificates.pem is in the current directory. If not, you need change Certificates.pem to /Users/path/Certificates.pem. Please make sure that your version of curl allows for http2.

curl -v -d '{"aps":{"alert":"hello"}}' --http2 --cert Certificates.pem:1234 https://api.development.push.apple.com/3/device/6a25342c9d1941c73b37c5c28f6788efe0f7a8e681ce440f15a8eb61e679cb63

Once you press enter, your iPhone will start to ring and display the incoming call UI. You can even force quit out of the app, use the curl command from above, and it forces your app open (temporarily) with the incoming call UI.

pushkit-callkit

Now you know how to use CallKit to make your VoIP (video chat/audio chat) apps feel more native. As always if you have any questions or comments, please leave them below.