Notificaciones push - iOS
Visión general
iOS exige que las aplicaciones de VoIP utilicen PushKit para las notificaciones de llamadas entrantes y CallKit para mostrar la interfaz de usuario de la llamada del sistema. Estos dos marcos están estrechamente vinculados: Apple exige que cada notificación de VoIP dé lugar a un reportNewIncomingCall a CallKit. El incumplimiento de esta norma provoca que iOS revoque de forma permanente el envío de notificaciones push al dispositivo.
El Vonage Client SDK se integra con ambos marcos de trabajo. Tu aplicación se encarga de:
- Registrarse en el servicio de notificaciones por VoIP y reenviar el token a Vonage
- Recibir cargas útiles de notificaciones push y transmitirlas al SDK para su procesamiento
- Mostrar las llamadas entrantes mediante CallKit
- Gestión del ciclo de vida de la sesión de audio exclusivamente mediante las funciones de devolución de llamada de CallKit
- Mantener una sesión SDK válida para poder responder a las llamadas
Responsabilidades de los componentes
| Componente | Responsabilidad |
|---|---|
| PushKit | Reactiva la aplicación desde el estado de cierre o suspensión y envía la carga útil de la notificación push. |
| SDK de Vonage | Decodifica la carga útil de los mensajes «push», gestiona la sesión WebRTC y se encarga de la señalización de las llamadas |
| CallKit | Muestra la interfaz de usuario de las llamadas al sistema y gestiona la activación de sesiones de audio |
| Tu aplicación | Coordina todo lo anterior y gestiona el ciclo de vida de los JWT y las sesiones |
Configuración del proyecto
1 - Funcionalidades de Xcode
Activa lo siguiente en el archivo Firma y capacidades ficha:
Push Notifications → enabled
Background Modes → Voice over IP (checked)
→ Background processing (checked)
Tu archivo de derechos debe contener:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.pushkit.unrestricted-voip</key>
<true/>
</dict>
</plist>
Advertencia: En aps-environment Xcode establece automáticamente este valor en función de la configuración de aprovisionamiento. No lo modifiques manualmente.
Derecho restringido a VoIP
A partir de iOS 16, las aplicaciones sin el com.apple.developer.pushkit.unrestricted-voip Los derechos de acceso están limitados a un número reducido de notificaciones PushKit al día. Si se supera el límite del sistema, el envío de notificaciones se ralentiza de forma silenciosa y las llamadas entrantes no activarán la aplicación.
Este derecho debe solicitarse a Apple a través de la formulario de solicitud de prestaciones. Hasta que se conceda, las compilaciones de desarrollo pueden funcionar con normalidad (las compilaciones de depuración tienen un presupuesto mayor), pero Las compilaciones de producción se limitarán.
Asegúrate de que este derecho figure en tu .entitlements presentar y que Apple lo apruebe antes de publicarlo en la App Store.
2 - Certificados Push
Necesitas un certificado APNs para tu App ID, exportado como un .p12, y se ha subido a la aplicación de Vonage con la que se autentica tu aplicación. Un certificado SSL de APNs moderno, con ámbito de aplicación, es válido durante .voip tema, por lo que se requiere un «Certificado de servicios de VoIP» independiente y heredado que sea no obligatorio.
Generar un certificado CSR
En Acceso a Llaveros → Asistente de certificados → Solicitar un certificado a una autoridad de certificación, introduce tu correo electrónico y un «Nombre común», deja en blanco el campo «Correo electrónico de la CA» y selecciona Guardado en el disco, y guarda el .certSigningRequest.
Crear el certificado
- En el Portal para desarrolladores de Apple → Certificados, identificadores y perfiles, asegúrate de que tu ID de la aplicación tiene el Notificaciones Push función activada.
- En Certificados, añade un Servicio de notificaciones push de Apple con SSL (entorno de pruebas y producción) certificado, selecciona tu ID de aplicación y sube la CSR.
- Descarga el archivo resultante
.cer.
Exportar el .p12
Haz doble clic en el .cer para importarla a tu llavero de inicio de sesión. En Mis certificadosencuentra Apple Push Services: <your-bundle-id> — debe tener la clave privada anidada debajo —; a continuación, haz clic con el botón derecho → Exportar → Intercambio de información personal (.p12). Deja en blanco la contraseña de exportación si es posible (consulta la nota sobre la subida de archivos que aparece a continuación).
Subir a través del panel de control de Vonage
- Inicie sesión en Panel de Vonage.
- Ir a Applications → tu solicitud (el
application_id(con el que se autentica el JWT de tu aplicación). - Abra el Activar las notificaciones push pestaña y sube el
.p12. - El campo de la contraseña es opcional y está etiquetado como No recomendable (Vonage lo almacena junto con tu clave privada), por lo que es preferible utilizar un certificado sin contraseña.
Véase el Documentación sobre la configuración de notificaciones push de Vonage para iOS A modo de referencia.
Los certificados caducan cada año. El modo de fallo es completo y silencioso — No aparece ningún error en ninguna parte del sistema. Realiza un seguimiento de la fecha de caducidad y renueva de forma proactiva.
Advertencia: Entorno de prueba frente a entorno de producción
Las compilaciones de depuración de Xcode utilizan el entorno APNs de prueba; las compilaciones de TestFlight y de la App Store utilizan el entorno de producción. Subir certificados independientes para cada entorno y determinar siempre el isSandbox indicador en tiempo de compilación utilizando #if DEBUG — Nunca lo introduzcas directamente en el código.
3 - Marcos de trabajo
Añadir CallKit debajo de Marcos, bibliotecas y contenidos integrados en la pestaña «General» de tu destino. Importe según sea necesario:
import PushKit
import CallKit
import VonageClientSDKVoice
import AVFoundation
4 - Inicialización del SDK
Inicializar VGVoiceClient una vez, al principio del ciclo de vida de la aplicación (por ejemplo, en tu AppDelegate o en un gestor singleton). Configura su delegado antes de registrarte para recibir notificaciones push o crear sesiones:
let config = VGClientInitConfig(loggingLevel: .info)
#if targetEnvironment(simulator)
config.enableWebsocketInvites = true
#endif
let client = VGVoiceClient(config)
client.delegate = self
Solicita permiso para utilizar el micrófono en una fase temprana del ciclo de vida de la aplicación; lo ideal sería hacerlo en application(_:didFinishLaunchingWithOptions:):
AVAudioSession.sharedInstance().requestRecordPermission { granted in
print("Microphone permission: \(granted)")
}
Si el usuario deniega el acceso al micrófono, las llamadas se establecerán, pero la persona al otro lado de la línea no oirá nada.
Integración con PushKit
Regístrate para recibir notificaciones de VoIP
Crear un PKPushRegistry y configurar los tipos de notificaciones push que desee al iniciar la aplicación, antes de que llegue ninguna notificación.
#if DEBUG
let isSandbox = true
#else
let isSandbox = false
#endif
client.registerVoipToken(pushToken, withDeviceToken: nil, isSandbox: isSandbox) { error, deviceId in
if let error = error {
print("Failed to register push token: \(error.localizedDescription)")
return
}
// Store deviceId for unregistration on logout
print("Push token registered, deviceId: \(deviceId ?? "")")
}
PKPushRegistry *pushRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
pushRegistry.delegate = self;
pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
PKPushRegistryDelegate
Implementa los tres métodos delegados. El tercero — didReceiveIncomingPushWith — es el más importante:
extension AppDelegate: PKPushRegistryDelegate {
// Called when a new VoIP token is issued or rotated
func pushRegistry(_ registry: PKPushRegistry,
didUpdate pushCredentials: PKPushCredentials,
for type: PKPushType) {
guard type == .voIP else { return }
CallManager.shared.voipTokenDidUpdate(pushCredentials.token)
}
// Called when the token is invalidated (e.g. app reinstall)
func pushRegistry(_ registry: PKPushRegistry,
didInvalidatePushTokenFor type: PKPushType) {
CallManager.shared.unregisterDeviceToken()
}
// Called when an inbound VoIP push is received
func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void) {
guard type == .voIP else { completion(); return }
CallManager.shared.handleIncomingPush(payload: payload, completion: completion)
}
}
Registro del token en Vonage
Regístrate en client.registerVoipToken(_:isSandbox:) solo cuando haya una sesión de Vonage activa. Guarda el valor devuelto deviceId — lo necesitas para darte de baja al cerrar sesión.
import PushKit
let pushRegistry = PKPushRegistry(queue: .main)
pushRegistry.delegate = self
pushRegistry.desiredPushTypes = [.voIP]
#if DEBUG
BOOL isSandbox = YES;
#else
BOOL isSandbox = NO;
#endif
[client registerVoipToken:pushToken withDeviceToken:nil isSandbox:isSandbox callback:^(NSError * _Nullable error, NSString * _Nullable deviceId) {
if (error != nil) {
NSLog(@"Failed to register push token: %@", error);
return;
}
// Store deviceId for unregistration on logout
NSLog(@"Push token registered, deviceId: %@", deviceId);
}];
No llames registerVoipToken en cada restauración de sesión. Una vez registrado, el token sigue siendo válido hasta que se renueve o hasta que el usuario cierre la sesión. Volver a registrarlo en cada createSession acumula entradas de dispositivos y puede provocar envíos duplicados.
Gestión de sesiones
Una sesión de Vonage (client.createSession(jwt)) es necesario para que el SDK pueda procesar invitaciones de llamada o realizar acciones relacionadas con las llamadas. Cuando llega una notificación de VoIP y la aplicación se cierra o la sesión ha caducado, es necesario volver a crear la sesión. La rapidez con la que esto ocurre determina directamente si los usuarios pueden responder a las llamadas con éxito.
El fallo de producción más habitual
No hagas lo siguiente:
Al recibir una notificación push: llamar al servidor de autenticación -> obtener un JWT nuevo -> llamar createSession. Esto añade dos saltos de red en serie en una ventana de ejecución en segundo plano con mala señal. El usuario ve la pantalla de CallKit, pulsa «Responder» antes de que la sesión esté lista y la llamada falla sin que se avise.
Qué hacer:
Al iniciar sesión: almacenar el JWT (y un token de actualización) localmente. Al recibir una notificación push: llamar a createSession directamente con el JWT almacenado. Sin llamadas al backend en la ruta rápida.
JWT Strategy
Sigue una cadena de prioridades: utiliza la ruta más rápida disponible:
| Prioridad | Condición | Acción | Latencia típica |
|---|---|---|---|
| 1.º | Hay una sesión activa | Devuélvelo inmediatamente — no hace falta nada más | 0 ms |
| 2.º | El JWT almacenado es válido | client.createSession(storedJWT) |
100-400 ms |
| 3.º | El JWT ha caducado, hay un token de actualización disponible | Actualizar punto final -> nuevo JWT -> createSession |
300-1000 ms |
| 4.º | Sin credenciales | Advertencia de registro: no se puede responder a la llamada | 0 ms |
Creación de sesiones
do {
let sessionId = try await client.createSession(jwt)
print("Session created: \(sessionId)")
} catch {
print("Failed to create session: \(error.localizedDescription)")
}
[client createSession:jwt callback:^(NSError * _Nullable error, NSString * _Nullable sessionId) {
if (error != nil || sessionId == nil) {
NSLog(@"Failed to create session: %@", error);
return;
}
NSLog(@"Session created: %@", sessionId);
}];
Restablecimiento de la sesión al recibir una notificación push
func restoreSessionIfNeeded() {
guard activeSessionId == nil else {
print("Session already active")
return
}
if let jwt = UserDefaults.standard.string(forKey: "vonage_jwt") {
// Fast path — use stored JWT directly
createSession(jwt: jwt)
} else if let refreshToken = storedRefreshToken {
// Slow path — exchange refresh token for a new JWT first
refreshJWT(refreshToken) { [weak self] newJWT in
self?.createSession(jwt: newJWT)
}
} else {
print("No credentials available for session restoration")
}
}
Procesamiento de cargas útiles de Push
Esta es la parte de la integración en la que el tiempo es más importante. Deben realizarse dos operaciones a partir de handleIncomingPush, y la relación de orden entre ellos no resulta evidente.
El patrón concurrente (obligatorio)
Nunca esperes a que se restablezca la sesión antes de llamar a processCallInvitePushData. Retrasar el procesamiento por impulsos createSession retrasos reportNewIncomingCall por una ronda completa de ida y vuelta en la red. Apple interpreta esto como un fallo en la gestión de la notificación push, por lo que PushKit deja de enviar silenciosamente futuras notificaciones push al dispositivo.
Aplicación
client.processCallInvitePushData(payload.dictionaryPayload)
[client processCallInvitePushData:payload.dictionaryPayload];
El patrón de implementación completo:
private var pendingPushCompletion: (() -> Void)?
func handleIncomingPush(payload: PKPushPayload, completion: @escaping () -> Void) {
// 1. Validate: only handle Vonage incoming-call pushes
let pushType = VGVoiceClient.vonagePushType(payload.dictionaryPayload)
guard pushType == .incomingCall else {
// Must still report to CallKit — failing to do so breaks future push delivery.
let fakeUUID = UUID()
callProvider.reportNewIncomingCall(with: fakeUUID, update: CXCallUpdate()) { _ in
self.callProvider.reportCall(with: fakeUUID, endedAt: .now, reason: .failed)
completion()
}
return
}
// 2. Store completion — will be called after reportNewIncomingCall
pendingPushCompletion = completion
// 3a. Restore session concurrently — non-blocking
restoreSessionIfNeeded()
// 3b. Process the push immediately — triggers didReceiveInviteForCall
client.processCallInvitePushData(payload.dictionaryPayload)
}
Advertencia: Aunque la entrada tenga un formato incorrecto, haya caducado o sea un duplicado, tú debe seguir llamando reportNewIncomingCall y colgar inmediatamente. Si se omite este paso, la entrega push dejará de funcionar de forma permanente.
Integración con CallKit
Configuración de CXProvider
Crear CXProvider y CXCallController una sola vez y consérvalas durante todo el ciclo de vida de tu gestor. No las vuelvas a crear en cada llamada o envío.
class CallManager: NSObject {
static let shared = CallManager()
private let callProvider: CXProvider
private let callController = CXCallController()
private override init() {
let config = CXProviderConfiguration()
config.supportsVideo = false
config.maximumCallGroups = 1
config.maximumCallsPerCallGroup = 1
config.supportedHandleTypes = [.generic]
config.supportsDTMF = true
config.supportsHolding = true
callProvider = CXProvider(configuration: config)
super.init()
callProvider.setDelegate(self, queue: nil)
}
}
Notificar una llamada entrante
Llamada desde didReceiveInviteForCall (véase la sección «Delegado del SDK»). La llamada a completion de PushKit debe invocarse dentro de la reportNewIncomingCall callback — nunca antes.
let update = CXCallUpdate()
let handleType: CXHandle.HandleType = channelType == .phone ? .phoneNumber : .generic
update.remoteHandle = CXHandle(type: handleType, value: caller)
update.hasVideo = false
provider.reportNewIncomingCall(with: callId, update: update) { error in
if let error = error {
print("Failed to report incoming call: \(error.localizedDescription)")
}
// Always call the PushKit completion inside this callback
pushCompletion()
}
CXCallUpdate *update = [[CXCallUpdate alloc] init];
CXHandleType handleType = (channelType == VGVoiceChannelTypePhone) ? CXHandleTypePhoneNumber : CXHandleTypeGeneric;
update.remoteHandle = [[CXHandle alloc] initWithType:handleType value:caller];
update.hasVideo = NO;
[provider reportNewIncomingCallWithUUID:callId update:update completion:^(NSError * _Nullable error) {
if (error != nil) {
NSLog(@"Failed to report incoming call: %@", error);
}
// Always call the PushKit completion inside this callback
pushCompletion();
}];
Tipo CXHandle
Uso .phoneNumber para el tipo CXHandle cuando VGVoiceChannelType es .phone (Llamadas de la red PSTN con números E.164). Utiliza .generic para llamadas entre aplicaciones. Esto afecta a la forma en que iOS muestra al autor de la llamada en la interfaz de usuario nativa de llamadas y en el historial de llamadas recientes.
CXProviderDelegate - Acciones del usuario
extension CallManager: CXProviderDelegate {
// Called when CallKit resets (e.g. during system errors or when the
// provider is invalidated). Clean up all active call state.
func providerDidReset(_ provider: CXProvider) {
activeCall = nil
}
// User tapped Answer
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
guard let callId = activeCallId else { action.fail(); return }
client.answer(callId) { error in
if error == nil {
action.fulfill()
} else {
action.fail()
}
}
}
// User tapped End (covers both Reject while ringing and Hang up during call)
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
guard let callId = activeCallId else { action.fail(); return }
let complete: (Error?) -> Void = { error in
error == nil ? action.fulfill() : action.fail()
}
if isCallRinging {
client.reject(callId, callback: complete)
} else {
client.hangup(callId, callback: complete)
}
}
// Mute toggle
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
guard let callId = activeCallId else { action.fail(); return }
if action.isMuted {
client.mute(callId) { _ in action.fulfill() }
} else {
client.unmute(callId) { _ in action.fulfill() }
}
}
// Hold toggle
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
guard let callId = activeCallId else { action.fail(); return }
if action.isOnHold {
client.enableEarmuff(callId) { _ in action.fulfill() }
} else {
client.disableEarmuff(callId) { _ in action.fulfill() }
}
}
// DTMF
func provider(_ provider: CXProvider, perform action: CXPlayDTMFCallAction) {
guard let callId = activeCallId else { action.fail(); return }
client.sendDTMF(callId, withDigits: action.digits) { _ in action.fulfill() }
}
}
Transferencia de sesiones de audio
// In CXProviderDelegate - provider(_:didActivate:)
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
VGVoiceClient.enableAudio(audioSession)
}
// In CXProviderDelegate - provider(_:didDeactivate:)
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
VGVoiceClient.disableAudio(audioSession)
}
// In CXProviderDelegate - provider:didActivateAudioSession:
[VGVoiceClient enableAudio:audioSession];
// In CXProviderDelegate - provider:didDeactivateAudioSession:
[VGVoiceClient disableAudio:audioSession];
Siempre activar y desactivar el audio de Vonage exclusivamente dentro de didActivate / didDeactivate. Si se activa el audio antes de que CallKit inicie la sesión, o desde un controlador de botón de la interfaz de usuario, se producen errores de enrutamiento de audio con Bluetooth, AirPods y CarPlay.
Delegado del SDK
VGVoiceClientDelegate - Eventos de llamadas entrantes
extension CallManager: VGVoiceClientDelegate {
func voiceClient(_ client: VGVoiceClient,
didReceiveInviteForCall callId: VGCallId,
from caller: String,
with type: VGVoiceChannelType) {
guard let callUUID = UUID(uuidString: callId) else { return }
activeCallId = callId
activeCallUUID = callUUID
isCallRinging = true
// Report to CallKit, then call the PushKit completion in the callback
reportIncomingCall(callUUID: callUUID, caller: caller, type: type,
pushCompletion: pendingPushCompletion ?? {})
pendingPushCompletion = nil
}
// Remote party hung up or the call timed out
func voiceClient(_ client: VGVoiceClient,
didReceiveHangupForCall callId: VGCallId,
withQuality callQuality: VGRTCQuality,
reason: VGHangupReason) {
guard let uuid = activeCallUUID else { return }
let cxReason: CXCallEndedReason = switch reason {
case .remoteReject: .declinedElsewhere
case .remoteHangup, .localHangup: .remoteEnded
case .remoteNoAnswerTimeout: .unanswered
default: .failed
}
callProvider.reportCall(with: uuid, endedAt: .now, reason: cxReason)
cleanUpCallState()
}
// Caller cancelled the invite before it was answered
func voiceClient(_ client: VGVoiceClient,
didReceiveInviteCancelForCall callId: VGCallId,
with reason: VGVoiceInviteCancelReason) {
guard let uuid = activeCallUUID else { return }
let cxReason: CXCallEndedReason = switch reason {
case .answeredElsewhere: .answeredElsewhere
case .rejectedElsewhere: .declinedElsewhere
case .remoteCancel: .remoteEnded
case .remoteTimeout: .unanswered
default: .failed
}
callProvider.reportCall(with: uuid, endedAt: .now, reason: cxReason)
cleanUpCallState()
}
private func cleanUpCallState() {
activeCallId = nil
activeCallUUID = nil
isCallRinging = false
}
}
Llama siempre callProvider.reportCall(with:endedAt:reason:) para cada Ruta de finalización: colgar desde el lado remoto, cancelación remota, tiempo de espera de medios, rechazo local. Si falta alguno de estos elementos, CallKit queda en un estado incoherente y puede afectar al registro de llamadas y a los futuros informes de llamadas.
Llamadas salientes
En el caso de las llamadas salientes, el proceso es inverso: tu aplicación inicia la llamada a través del SDK y, a continuación, utiliza CXCallController para informar a CallKit.
// 1. Initiate the call via the SDK
func startCall(to callee: String) {
let callContext = ["callee": callee]
client.serverCall(callContext) { [weak self] error, callId in
guard let callId,
let callUUID = UUID(uuidString: callId) else { return }
self?.activeCallId = callId
self?.activeCallUUID = callUUID
// 2. Notify CallKit so the in-call UI appears
let handle = CXHandle(type: .generic, value: callee)
let action = CXStartCallAction(call: callUUID, handle: handle)
let transaction = CXTransaction(action: action)
self?.callController.request(transaction) { [weak self] error in
guard error == nil else { return }
self?.callProvider.reportOutgoingCall(with: callUUID, startedConnectingAt: .now)
}
}
}
// 3. CXStartCallAction — CallKit confirming the transaction
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
action.fulfill()
}
// 4. When the remote side answers
func voiceClient(_ client: VGVoiceClient,
didReceiveLegStatusUpdateForCall callId: VGCallId,
withLegId legId: String,
andStatus status: VGLegStatus) {
guard status == .answered, let uuid = activeCallUUID else { return }
callProvider.reportOutgoingCall(with: uuid, connectedAt: .now)
}
Gestión de errores
El SDK se activa didReceiveSessionErrorWith(reason:) cuando se interrumpe la conexión WebSocket subyacente durante una sesión activa. Intenta siempre restablecer la sesión automáticamente.
func client(_ client: VGBaseClient,
didReceiveSessionErrorWith reason: VGSessionErrorReason) {
print("Session error: \(reason)")
switch reason {
case .tokenExpired:
// The stored JWT is now invalid — skip it, use refresh token
refreshAndRestoreSession()
case .pingTimeout, .transportClosed:
// Transient network issue — try the stored JWT first
restoreSessionIfNeeded()
default:
restoreSessionIfNeeded()
}
}
private func refreshAndRestoreSession() {
guard let refreshToken = storedRefreshToken else {
clearSession()
return
}
refreshJWT(refreshToken) { [weak self] result in
switch result {
case .success(let newJWT):
self?.createSession(jwt: newJWT)
case .failure:
self?.clearSession()
}
}
}
Limitaciones del simulador
PushKit y CallKit no están disponibles en el simulador de iOS. El SDK ofrece un modo de invitación mediante WebSocket para el desarrollo:
let config = VGClientInitConfig(loggingLevel: .info)
#if targetEnvironment(simulator)
config.enableWebsocketInvites = true // receive invites over WebSocket — no push needed
#endif
let client = VGVoiceClient(config)
func answerCall(callId: String) {
#if targetEnvironment(simulator)
client.answer(callId) { error in
print("Answer: \(error?.localizedDescription ?? "OK")")
}
#else
guard let uuid = activeCallUUID else { return }
let action = CXAnswerCallAction(call: uuid)
callController.request(CXTransaction(action: action)) { _ in }
#endif
}
Advertencia: Comprueba siempre todo el proceso de envío en un dispositivo físico antes del lanzamiento. El simulador no puede simular la reactivación de una aplicación cerrada, la restauración de la sesión con un presupuesto de fondo real ni la interfaz de usuario de CallKit en la pantalla de bloqueo.
Errores y antipatrones
1 - Llamada a la función «PushKit Completion» antes de reportNewIncomingCall
No hagas lo siguiente:
func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void) {
client.processCallInvitePushData(payload.dictionaryPayload)
completion() // WRONG — called before reportNewIncomingCall
}
Guarda la función de finalización y ejecútala exclusivamente dentro de reportNewIncomingCallla llamada de retorno asíncrona de '.
2 - Saltar a la comba reportNewIncomingCall para envíos no válidos
No hagas lo siguiente:
guard pushType == .incomingCall else {
completion() // WRONG — no call reported to CallKit
return
}
Qué hacer: Notificar una llamada ficticia y finalizarla inmediatamente; a continuación, llamar a la función de finalización:
guard pushType == .incomingCall else {
let fakeUUID = UUID()
callProvider.reportNewIncomingCall(with: fakeUUID, update: CXCallUpdate()) { _ in
self.callProvider.reportCall(with: fakeUUID, endedAt: .now, reason: .failed)
completion()
}
return
}
3 - A la espera de que se restablezca la sesión antes de procesar la notificación
No hagas lo siguiente:
restoreSessionIfNeeded { // Waits for network...
self.client.processCallInvitePushData(payload) // Too late
}
Qué hacer: Disparar ambos a la vez:
restoreSessionIfNeeded() // Non-blocking
client.processCallInvitePushData(payload.dictionaryPayload) // Immediate
4 - Obtención de un JWT del backend al recibir una notificación push
No hagas lo siguiente:
func handleIncomingPush(payload: PKPushPayload, completion: @escaping () -> Void) {
fetchJWTFromServer { jwt in // 500ms - 3s network call
self.client.createSession(jwt) // Another network call
self.client.processCallInvitePushData(payload.dictionaryPayload)
}
}
Qué hacer: Conservar el JWT en el momento del inicio de sesión. Utilizarlo directamente en la notificación push sin realizar ninguna llamada al servidor.
5 - Codificación fija isSandbox: true
No hagas lo siguiente:
client.registerVoipToken(token, isSandbox: true) // Breaks TestFlight
Qué hacer:
#if DEBUG
let isSandbox = true
#else
let isSandbox = false
#endif
client.registerVoipToken(token, isSandbox: isSandbox)
6 - Volver a registrar el token de Push cada vez que se restablece una sesión
No hagas lo siguiente:
func createSession(jwt: String) {
client.createSession(jwt) { _, _ in
self.registerVoipToken(self.storedToken) // Accumulates device entries
}
}
Qué hacer: Regístrate solo al iniciar sesión o cuando didUpdate pushCredentials disparos (rotación de fichas).
7 - Olvidarse de darse de baja al cerrar sesión
Si te saltas unregisterDeviceTokens(byDeviceId:) Al cerrar sesión, el dispositivo sigue recibiendo notificaciones VoIP de ese usuario de forma indefinida, incluso después de que otro usuario haya iniciado sesión.
8 - No se han asignado todos los motivos de colgado a CallKit
Cada ruta de terminación de llamada debe llamar a callProvider.reportCall(with:endedAt:reason:) con el correcto CXCallEndedReason. Si falta una ruta, se corrompe el registro de llamadas al sistema.
9 - Activación del audio fuera de las llamadas de retorno de CallKit
No hagas lo siguiente:
func answerButtonTapped() {
VGVoiceClient.enableAudio(AVAudioSession.sharedInstance()) // WRONG
client.answer(callId) { _ in }
}
Qué hacer: Gestionar el audio exclusivamente en el delegado de CallKit:
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
VGVoiceClient.enableAudio(audioSession)
}
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
VGVoiceClient.disableAudio(audioSession)
}
10 - Certificados push caducados
Los certificados de VoIP caducan al cabo de un año. El fallo es total y silencioso. Renuévalos de forma proactiva y Verify que se han enviado correctamente inmediatamente después de cargar un nuevo certificado.
Lista de comprobación de la producción
Utiliza esta lista de comprobación antes de realizar el envío:
- [ ] Funciones de Xcode configuradas — Notificaciones push activadas; Modos en segundo plano: Voz sobre IP + Procesamiento en segundo plano marcados
- [ ] Se ha subido el certificado «push» de VoIP para ambos entornos — Entorno de prueba para la depuración; entorno de producción para TestFlight y la App Store
- [ ]
isSandboxusos de la bandera#if DEBUG— nunca se codifica de forma fija - [ ] El token JWT y el token de actualización se almacenan localmente al iniciar sesión — La restauración de la sesión no debe requerir una llamada al backend
- [ ]
reportNewIncomingCallse exigió que se realizara cada envío de VoIP sin excepción — incluyendo notificaciones no válidas, duplicadas o que no procedan de Vonage - [ ] La función de autocompletado de PushKit solo se invoca dentro de
reportNewIncomingCallla función de devolución de llamada — nunca antes, nunca se había saltado - [ ] La restauración de sesiones y el procesamiento por empuje se realizan de forma simultánea -
restoreSessionIfNeeded()yprocessCallInvitePushData()disparar de forma independiente - [ ] El registro del token solo se realiza al iniciar sesión o al renovar el token — no en todos los
createSession - [ ]
unregisterDeviceTokensse invocó al cerrar sesión explícitamente - [ ] Todas las rutas de terminación de llamadas se comunican a CallKit — desconexión remota, cancelación, tiempo de espera agotado, rechazo local, fallo en el medio de comunicación
- [ ] El audio se activa o desactiva únicamente en
didActivate/didDeactivate - [ ] Código de CallKit protegido con
#if !targetEnvironment(simulator)-enableWebsocketInvites = truepara el simulador - [ ] Probado en un dispositivo físico con la aplicación completamente cerrada — Ejercicios sobre la carrera de activación y restauración de sesión de PushKit
- [ ] Probado en un dispositivo físico con la aplicación en segundo plano (en suspensión)
- [ ] Se realiza un seguimiento de la fecha de renovación del certificado Push — Los certificados caducan al cabo de un año.