Реалізація 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 та реальних кейсах.
Як довго триває інтеграція 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, тому знаємо всі підводні камені.







