PKPushRegistryDelegate and no CXProviderDelegate.
On iOS the ring is always CallKit’s native incoming-call screen, in every app state. There is no custom incoming-call screen; your own UI starts when the user answers.
This page covers the push side of ringing. Starting, answering and ending calls with the Calls SDK is on the Ringing page.
Prerequisites
- Calls SDK 5.1 set up and the user logged in.
- Push Notifications enabled in the CometChat dashboard with an APNs provider that uses a
.p8auth key, and that provider’s id. The SDK registers both the APNs device token and the VoIP token under this one provider; a.p12certificate provider can serve only one of them.- The provider’s Bundle ID must be your app’s bundle ID. VoIP pushes go to the topic
<bundle id>.voip. - The provider’s APNs environment must match the build. A build run from Xcode gets a sandbox token, so it needs a provider with Enable Production Mode off. TestFlight and App Store builds need it on. On a mismatch APNs rejects the push and nothing rings.
- The provider’s Bundle ID must be your app’s bundle ID. VoIP pushes go to the topic
- iOS 15.1 or later.
- A physical device. PushKit and CallKit do not work in the Simulator.
Step 1: Add the SDK
The Push Notifications SDK ships as three products. For ringing you add Core and Calls; add Chat as well if the app shows chat notifications.
The SDK is distributed with Swift Package Manager. In Xcode, File → Add Package Dependencies, enter:
CometChatPushNotificationsSwift and CometChatPushNotificationsCalls to your app target. The Calls product brings the Calls SDK 5.1 with it.
Linking the Calls product turns ringing on. The Chat SDK is optional: a calls-only app adds Core and Calls and never links it. import CometChatPushNotificationsCalls also brings the call types (IncomingCall, CallEndReason) with it.
The SDK links PushKit, CallKit and AVFAudio itself; you add nothing for them.
CallKit is not allowed in apps sold in China. For a China App Store build, ship a target without the Calls product; it is the only product that uses CallKit.
Step 2: Capabilities and Info.plist
Add all of these. Each one, when missing, silences calls without an error.Info.plist
aps-environment = development; TestFlight and App Store builds need production, which Xcode sets when you archive with a distribution profile.
Step 3: Initialise in didFinishLaunching
Call initialize(config:) in application(_:didFinishLaunchingWithOptions:), on every launch, before the user logs in. This is the one hard rule:
- The SDK creates its PushKit registry inside
initialize. iOS delivers the VoIP push that launched a killed app only to a registry that exists by the timedidFinishLaunchingreturns, so a later call loses the first call after a cold start. - Token registration on login and logout only runs in a launch where
initializeran. initializealso sets itself as theUNUserNotificationCenterdelegate, asks for notification permission (unless you turn it off), and saves the configuration so a launch started by a push still knows your app.
AppDelegate below does.
- Swift
- Objective-C
AppDelegate.swift
AppDelegate attached with @UIApplicationDelegateAdaptor.
PNConfiguration(appId:region:…) takes every option below as a defaulted parameter. From Objective-C, or to chain setters, use PNConfiguration.builder(appId:region:) and finish with build(). If you already have the Calls SDK’s CallAppSettings, PNConfigurationBuilder.from(_:) starts a builder with its app id and region (it returns nil if they are missing).
A call rings for the ring timeout the push carries, or 45 seconds (
RingConstants.defaultRingTimeoutMs) when it carries none, counted from when the caller started the call rather than from when the push arrived (the push’s send time, or failing that its arrival, when the start time is missing), plus about 2 seconds of slack.
If the app also shows chat notifications, forward the APNs device token from your AppDelegate as described in iOS Push Notifications. The VoIP token needs nothing: the SDK receives it from its own PushKit registry.
Step 4: Join when the user answers
On Answer the SDK callspresentCall(_:) on your delegate. It fires at once in every app state (foreground, background, launched from killed, and with the phone locked), on the main queue, and at most once per call. Present your call screen and join at once: the join connects the audio while the phone may still be locked, and the screen becomes visible when the user unlocks. There is nothing to “accept”.
Adopt CometChatPushNotificationsCallsDelegate on the object you set as the delegate:
PushDelegate.swift
.failed if the session has not joined within 20 seconds, so if your UI may come up later than that (an answer from the lock screen of an app the push just started), start the join without waiting for the window: create the call screen, call joinSession with its container view, and present it once a window exists.
CallPresenter.swift
CallViewController.swift
IncomingCall gives you sessionId, callerUid, callerName, callerAvatarUrl, callType, isVideo, isPeerCall (a 1:1 call), receiverType, receiverUid and conversationId.
While the phone rings, the SDK already connects to the call.
joinSession(sessionID:) takes that prepared session over, so answering only adds the media. A prepared session keeps the settings it was prepared with (the session type, video paused for a voice call, speaker audio, and a peer call for 1:1), and the settings passed to joinSession are ignored for it.CallSession.shared.leave()), or let the user end the call from the CallKit screen. Either way the SDK ends the CallKit call and calls callDidEnd.
Answer without a join. With no delegate adopting CometChatPushNotificationsCallsDelegate, or when presentCall returns false, the SDK ends the CallKit call at once and calls callDidEnd with .failed. If presentCall returned true but the session has not joined within 20 seconds of Answer, it does the same, rather than leave a silent call on screen. So return true and defer the presentation when your UI is not ready yet, as the sample above does, instead of returning false.
callDidEnd(_:reason:) gets one of:
.answered is never delivered.
Permissions and ring readiness
Microphone and camera. The SDK asks after Answer, without holding up the join. On a lock-screen answer the prompt appears when the user unlocks. A denied microphone joins muted and does not fail the call. Ask earlier as well (at sign-in, for example) so a call is not where the user first meets the prompt. If your app asks itself, turn the SDK prompt off withrequestCallPermissions: false.
Notifications. Calls ring through CallKit whether or not the user allows notifications. Notification permission matters only for chat notifications.
Ring readiness. Several build and device settings silence a call with no error anywhere. Check once after sign-in and tell the user:
For a bug report,
CometChatPushNotifications.shared.getDiagnostics() returns a dictionary of everything that affects ringing (products installed, login state, cached tokens, background modes, entitlement, permissions, usage strings), with no token or auth value in it. verifyPushSetup(completion:) registers the tokens and reports .notLoggedIn, .tokenNotRegistered or .testPushUnavailable; the last means registration succeeded but a round-trip test push is not available, so it is the success result today.
How a ring ends
The SDK handles every way a ring ends; you write no code for it.- Decline, 1:1 call. The SDK sends
rejectedto the caller, orbusywhen the user is in another call. A decline that iOS suspends the app before sending is retried when the app next launches, if that is within 90 seconds. - Decline, group call. The ring stops on this device only. Nothing is sent, and the call continues for the others.
- No answer, 1:1 call. After the ring window the CallKit call ends as unanswered, which leaves a missed call in Recents, and the SDK sends
unanswered. - No answer, group call. The ring stops and leaves a missed call; nothing is sent.
- Caller cancels. The ring stops and leaves a missed call.
- Answered on another device. The ring stops here, with no missed call.
- Late push. A push that arrives after its ring window is not rung; it leaves a missed call.
callDidEnd.
A call during a call
A call that arrives while the user is in a CometChat call rings over it on CallKit’s call-waiting screen with Decline and End & Accept. There is no Hold & Accept: the Calls SDK runs one session at a time.- Decline sends
busyto a 1:1 caller; the live call continues. - End & Accept ends the live call (you get
callDidEndfor it), waits for its session to leave, then deliverspresentCallfor the new call.
callDidEnd, and nothing is sent to its caller.
A cellular call that arrives during a CometChat call puts it on hold: the SDK mutes the microphone and pauses the video, and restores both when the cellular call ends.
Missed calls
The SDK posts no missed-call notification on iOS. CallKit writes the missed call into the Phone app’s Recents when a call stops ringing unanswered (timed out, cancelled by the caller, or ended while still ringing), as long asincludesCallsInRecents is true (the default). Declined calls and calls answered elsewhere leave no missed call.
Your app gets callDidEnd with .timedOut, .cancelled or .remoteEnded (ended while still ringing), which is where to update your own missed-call UI. Call history comes from Call Logs.
When the app is killed
PushKit launches the app in the background,didFinishLaunching runs initialize, and the push reaches the SDK’s registry, which reports the call to CallKit.
iOS requires every VoIP push to be reported to CallKit as a call; an app that does not is terminated, and iOS stops delivering VoIP pushes to it if it keeps happening. The SDK reports every VoIP push: one it does not ring (a duplicate, an expired call, a call to another user, a payload that is not a call) is reported and ended at once.
On a launch started by a push the SDK brings the Calls SDK up from the saved app id and region, and the Calls SDK restores the logged-in user from its own storage. Initialise the Calls SDK in your own startup code as usual as well, so the join after Answer finds it ready.
Tokens and logout
You do not register push tokens. The SDK registers the VoIP token and, when the Chat product is linked, the APNs device token, both under yourproviderId, when the user logs in to either SDK (or in initialize if someone is already logged in). It registers a rotated token again, and unregisters a VoIP token that iOS invalidates. A calls-only app registers only the VoIP token.
To log out, unregister first and wait for it: the request is authenticated with the user’s session, so it has to finish before that session ends.
.failed), tries to unregister the tokens and clears the app badge. That unregister can be too late because the session is already gone, which is why you call unregisterTokens first. The tokens stay cached on the device and are registered again for the next user who logs in.
Customising CallKit
The incoming screen is CallKit’s, so what you can change is CallKit’s configuration, throughCallKitBranding:
The caller’s name on the ring comes from the push. Pass the branding as
callKit: in PNConfiguration, or with the builder’s callKit(_:) or callKit(name:iconTemplateImageName:ringtone:includeInRecents:supportsVideo:).
CometChatPushNotificationsCalls.decline(sessionId:) declines the ringing call exactly as CallKit’s Decline does, for your own in-app controls or tests. answer(sessionId:) exists for tests only: it answers without going through CallKit, so the CallKit call is not updated and the audio session is not configured for the call. Answer from the CallKit screen in apps.
Outgoing calls
CallKit owns the Calls SDK’s audio only while it has a call: from the moment an incoming call is answered or an outgoing call is reported to it, until the last such call ends. The SDK reports outgoing calls to CallKit itself, so they get CallKit-owned audio: it puts an outgoing call on CallKit when the Calls SDK connects or joins its session, including a session you join directly, such as a meeting. It reports it under your app’s name as a voice call and renames it when the other party joins. A session CallKit has no call for uses WebRTC’s normal audio handling. When the Calls product is linked, don’t setCometChatAudioSession.callKitAudioEnabled yourself: the SDK turns it on and off as CallKit’s calls come and go, and each write mutes WebRTC until CallKit next activates the audio session.
To give CallKit the callee’s name and the call type from the start, report the call yourself:
Knowing about a call in your own code
Listen on the Calls SDK, not on this SDK. It is the same event whether the call arrived by push on a killed app or on a running one:cometchat-push-notifications, which the Push Notifications SDK uses for its own listener. Remove yours with CometChatCalls.removeIncomingCallListener("my-screen") when the screen goes away.
Owning PushKit yourself
If your app already has aPKPushRegistry for other VoIP pushes, set pushKitOwnership: .app and forward to the SDK from your registry delegate:
handleIncomingVoIPPush returns false the SDK did not report anything and did not call completion, so reporting a call to CallKit is up to you. Your registry must still be created in didFinishLaunching.
Troubleshooting
Check the push itself
When a killed app does not ring, look at what the server sent before debugging the app. In the dashboard, open Notifications → Logs, search for the call’s message ID and find the row for the device’sAPNS_IOS_VOIP token.
- Success
True,200: APNs accepted the push. Check the app (the rows above). 400or403: APNs rejected the push, so the device was never woken. Fix the provider, not the app.- No row for the token: the token is not registered. See Tokens never register above.
Related
- Ringing: starting, answering and ending calls with the Calls SDK.
- Migrating from legacy ringing.
- Background Handling: keeping the call running when the user leaves the app.
- iOS Push Notifications: chat notifications with the same SDK.