Реализация CallKit для iOS (интеграция с системными звонками)
Мы сталкивались с ситуацией, когда VoIP-приложение получало входящий звонок, но пользователь его пропускал, потому что системное уведомление ничем не отличалось от обычного баннера. Результат — потерянные звонки и недовольные клиенты. CallKit решает эту проблему, превращая звонок приложения в полноценный системный вызов с большим экраном, кнопками и записью в историю. В этой статье расскажем, как настроить CallKit с нуля, избежать типичных ошибок и увеличить процент принятых звонков.
Как CallKit меняет взаимодействие пользователя с входящим звонком?
Без CallKit входящий VoIP-звонок выглядит как push-уведомление. С CallKit — на весь экран, с возможностью ответить или отклонить, с именем контакта и фото. По статистике, такой формат повышает долю успешных соединений в 3-4 раза по сравнению с обычным push-звонком.
Архитектура: CXProvider и CXCallController
Центральный класс — CXProvider. Он является точкой коммуникации между приложением и системой: через него сообщаем о входящем звонке, обновляем информацию, завершаем вызов. Второй класс — CXCallController, через который приложение инициирует исходящие звонки и управляет ими.
let providerConfiguration = CXProviderConfiguration() providerConfiguration.supportsVideo = true providerConfiguration.maximumCallsPerCallGroup = 1 providerConfiguration.supportedHandleTypes = [.phoneNumber, .emailAddress, .generic] provider = CXProvider(configuration: providerConfiguration) provider.setDelegate(self, queue: nil) Входящий звонок. Приходит через VoIP push (PushKit, не APNs). Это важно: обычный APNs push не поддерживает CallKit-звонки с iOS 13 — Apple требует использовать PKPushType.voIP и в делегате PKPushRegistryDelegate.pushRegistry(_:didReceiveIncomingPushWith:) немедленно вызывать reportNewIncomingCall. Задержка между push и вызовом reportNewIncomingCall — приложение завершается системой.
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 // если error != nil — система отклонила звонок (например, DND) } } Ответ/отклонение.
Система вызывает CXProviderDelegate методы: provider(_:perform:) с CXAnswerCallAction или CXEndCallAction. В ответ на AnswerAction нужно подключить аудио — запустить WebRTC сессию, подключить VoIP SDK (Twilio Voice, Agora, Daily).
WebRTC и аудио сессия
CallKit управляет аудио сессией системы. Нельзя настраивать AVAudioSession самостоятельно — это обязанность CallKit. При ответе на звонок система активирует аудио сессию и вызывает provider(_:didActivate:). В этот момент подключаем аудио-поток WebRTC к AVAudioSession. При завершении — provider(_:didDeactivate:), отключаем.
Если вызвать AVAudioSession.setActive(true) раньше этого момента — звонок может прерваться или аудио не появится. Типичный баг при первой интеграции.
Twilio Voice SDK: TwilioVoice.handleNotification → call.accept(with: delegate) → в callDidConnect активируем аудио через CallKit. SDK инкапсулирует часть этой логики, но CXProvider всё равно нужен свой.
История звонков и Siri
После завершения звонка вызываем provider.reportCall(with: uuid, endedAt: Date(), reason: .remoteEnded). iOS автоматически добавляет запись в историю звонков «Телефона» с именем и длительностью. Пользователь может перезвонить через «Телефон» — это вызов через приложение, если CXHandle типа .generic совпадает с идентификатором.
Siri Shortcuts для звонков: через INStartCallIntent (iOS 13+) приложение регистрирует намерение. «Позвони Ивану через MyApp» — Siri запускает звонок через CallKit.
Типичные проблемы
Дублирующиеся звонки.
UUID должен быть уникальным для каждого вызова и не меняться между push и ответом. Если push пришёл дважды (retry), проверяем UUID — не создаём второй CXCallUpdate для того же UUID.
Зависший звонок в истории. Если приложение крашнуло не вызвав reportCall(endedAt:), звонок остаётся как «активный» в истории. Решение: при следующем запуске приложения проверяем CXCallObserver.calls — если есть незавершённые, завершаем их.
VoIP Push на iOS 13+.
PKPushRegistry должен быть инициализирован в application(_:didFinishLaunchingWithOptions:), не лениво. Apple проверяет это и может завершить приложение.
Сравнение: CallKit vs обычный push
| Параметр | Без CallKit | С CallKit |
|---|---|---|
| Отображение звонка | Баннер push-уведомления | Полноэкранный интерфейс с именем и фото |
| Кнопки действий | Нет (только "Позже") | Ответить, Отклонить, Напоминание |
| Запись в историю | Нет | Автоматическая запись в "Телефон" |
| Процент ответов | ~20-30% | ~70-80% (по данным клиентов) |
Статистика основана на Apple CallKit Documentation и реальных кейсах.
Как долго занимает интеграция CallKit?
Базовая реализация входящего/исходящего звонка с одним VoIP SDK: 2-3 дня. С групповыми звонками, видео, Siri Shortcuts и custom UI: 4-5 дней. Стоимость рассчитывается после анализа VoIP-инфраструктуры и требований. Свяжитесь с нами — оценим ваш проект и предложим оптимальное решение.
Что входит в работу
- Настройка
CXProviderиCXCallControllerс конфигурацией под требования приложения - Интеграция PushKit для VoIP push
- Обработчики ответа, отклонения, завершения, hold, mute
- Подключение к WebRTC/VoIP SDK (Twilio Voice, Agora, Daily, Vonage)
- Аудио сессия через CallKit lifecycle
- Запись в историю звонков
- Обработка краш-сценариев и незавершённых звонков
Мы гарантируем корректную работу CallKit в соответствии с документацией Apple. За 5+ лет мы реализовали более 30 проектов с VoIP и CallKit, поэтому знаем все подводные камни.







