CallKit Implementation for iOS (System Calls Integration)
CallKit allows app to display incoming calls same as operator system calls: full screen, with contact name, answer/decline buttons, integration with phone's call history and blocking via "Blocked Contacts". Without CallKit incoming call looks like push notification — user taps "Later" not understanding it's a call.
How It Works Inside
Central class — CXProvider. It's communication point between app and system: report incoming call, update info, terminate call. Second class — CXCallController through which app initiates outgoing calls and manages them.
let providerConfiguration = CXProviderConfiguration()
providerConfiguration.supportsVideo = true
providerConfiguration.maximumCallsPerCallGroup = 1
providerConfiguration.supportedHandleTypes = [.phoneNumber, .emailAddress, .generic]
provider = CXProvider(configuration: providerConfiguration)
provider.setDelegate(self, queue: nil)
Incoming call. Arrives via VoIP push (PushKit, not APNs). This important: regular APNs push doesn't support CallKit calls since iOS 13 — Apple requires using PKPushType.voIP and in delegate PKPushRegistryDelegate.pushRegistry(_:didReceiveIncomingPushWith:) immediately call reportNewIncomingCall. Delay between push and reportNewIncomingCall — system terminates app.
func reportIncomingCall(uuid: UUID, handle: String, hasVideo: Bool) {
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: handle)
update.hasVideo = hasVideo
provider.reportNewIncomingCall(with: uuid, update: update) { error in
// if error != nil — system rejected call (e.g., DND)
}
}
Answer/decline. System calls CXProviderDelegate methods: provider(_:perform:) with CXAnswerCallAction or CXEndCallAction. On AnswerAction need to connect audio — start WebRTC session, connect VoIP SDK (Twilio Voice, Agora, Daily).
WebRTC and Audio Session
CallKit manages system audio session. Can't configure AVAudioSession independently — CallKit responsibility. On call answer system activates audio session and calls provider(_:didActivate:). At this moment connect WebRTC audio stream to AVAudioSession. On termination — provider(_:didDeactivate:), disconnect.
Calling AVAudioSession.setActive(true) before this moment — call may interrupt or audio missing. Typical bug on first integration.
Twilio Voice SDK: TwilioVoice.handleNotification → call.accept(with: delegate) → in callDidConnect activate audio via CallKit. SDK encapsulates part of logic but CXProvider still needed own.
Call History and Siri
After call termination call provider.reportCall(with: uuid, endedAt: Date(), reason: .remoteEnded). iOS automatically adds entry to "Phone" app call history with name and duration. User can call back via "Phone" — this call via app if CXHandle type .generic matches identifier.
Siri Shortcuts for calls: via INStartCallIntent (iOS 13+) app registers intent. "Call Ivan via MyApp" — Siri initiates call via CallKit.
Typical Problems
Duplicate calls. UUID must be unique per call and unchanged between push and answer. If push arrived twice (retry), check UUID — don't create second CXCallUpdate for same UUID.
Hung call in history. If app crashes not calling reportCall(endedAt:), call stays "active" in history. Solution: on next app launch check CXCallObserver.calls — if incomplete calls exist, terminate them.
VoIP Push on iOS 13+. PKPushRegistry must initialize in application(_:didFinishLaunchingWithOptions:), not lazily. Apple checks this can terminate app.
What's Included
-
CXProviderandCXCallControllersetup per app requirements - PushKit integration for VoIP push
- Handlers for answer, decline, termination, hold, mute
- WebRTC/VoIP SDK connection (Twilio Voice, Agora, Daily, Vonage)
- Audio session via CallKit lifecycle
- Call history recording
- Crash scenario and incomplete call handling
Timeline
Basic incoming/outgoing call with single VoIP SDK: 2–3 days. With group calls, video, Siri Shortcuts and custom UI: 4–5 days. Cost calculated after VoIP infrastructure analysis and requirements.







