
Compartilhar:
Abdul é um Developer Advocate da Vonage. Ele tem experiência profissional na área de produtos de consumo como engenheiro de iOS. Em seu tempo livre, ele gosta de andar de bicicleta, ouvir música e orientar quem está dando os primeiros passos na área de tecnologia
Como lidar com notificações push de VoIP usando o CallKit do iOS
Tempo de leitura: 8 minutos
Neste tutorial, você utilizará CallKit para lidar com as notificações push de VoIP enviadas a um dispositivo iOS ao usar o Vonage Client SDK para iOS. As notificações push de VoIP são o principal método para receber chamadas recebidas com o Client SDK, já que elas chegarão ao dispositivo independentemente de seu aplicativo estar em primeiro plano ou não. O CallKit permite que você integre seu aplicativo iOS ao sistema, de modo que ele pareça uma chamada telefônica nativa do iOS. Usar o Vonage Client SDK com o CallKit permitirá que você incorpore funcionalidades de chamada ao seu aplicativo, mantendo uma interface consistente e familiar para chamadas recebidas.
Pré-requisitos
Um account da API da Vonage. Se você ainda não tiver um, pode se cadastrar hoje mesmo.
Um account de desenvolvedor da Apple e um dispositivo iOS (ou simulador no Xcode 14 e versões posteriores).
Um account no GitHub.
Xcode 12 e Swift 5 ou versão posterior.
Cocoapods para instalar o Vonage Client SDK para iOS.
Nossa interface de linha de comando. Você pode instalá-la com
npm install -g @vonage/cli.
O Projeto Inicial
Este blog se baseará no tutorial “Recebendo uma chamada telefônica no aplicativo” do portal de desenvolvedores da Vonage. Este tutorial começará a partir do estado final do projeto do tutorial. Você pode acompanhar passo a passo ou, se já estiver familiarizado com a criação de um aplicativo de voz usando o Vonage Client SDK, pode clonar o no GitHub.
Configurar certificados de notificação push
Existem dois tipos de notificações push que você pode usar em um aplicativo iOS: notificações push de VoIP com o PushKit ou Notificações do Usuário. Este tutorial se concentrará nas notificações push de VoIP. O serviço de Notificações Push da Apple (APNs) usa autenticação baseada em certificado para proteger as conexões entre o APNs e os servidores da Vonage. Portanto, você precisará criar um certificado e enviá-lo aos servidores da Vonage para que a empresa possa enviar uma notificação push ao dispositivo quando houver uma chamada recebida.
Incorporando a funcionalidade de notificações push
Para usar notificações push, é necessário adicionar o recurso de notificações push ao seu projeto do Xcode. Certifique-se de estar conectado à sua conta de desenvolvedor da Apple no Xcode por meio das preferências. Se estiver, selecione seu destino e, em seguida, escolha Assinatura e Recursos:

Em seguida, selecione “Adicionar recurso” e adicione as Notificações push e Modos em segundo plano :

Na funcionalidade “Modos de fundo”, selecione Voz sobre IP e Processamento em segundo plano. Se o Xcode estiver gerenciando automaticamente a assinatura do seu aplicativo, ele atualizará o perfil de provisionamento vinculado ao seu Identificador de Pacote para incluir esses recursos.
Ao utilizar notificações push via VoIP, é necessário usar o framework CallKit. Vincule-o ao seu projeto adicionando-o em Frameworks, Bibliotecas e Conteúdo Incorporado em Geral:

Geração de um certificado de push
Para gerar um certificado push, você precisará fazer login na sua conta de desenvolvedor da Apple e acessar a seção página “Certificados, Identificadores e Perfis” e adicionar um novo certificado:

Escolha Serviço de Notificação Push da Apple SSL (Sandbox e Produção) e continue.

Agora você precisará escolher o App ID do aplicativo ao qual deseja adicionar notificações push de VoIP e continuar. Se o seu aplicativo não estiver na lista, você precisará criar um App ID. O Xcode pode fazer isso por você, caso gerencie automaticamente sua assinatura. Caso contrário, você pode criar um novo App ID na seção página “Certificados, Identificadores e Perfis” , na seção Identificadores. Certifique-se de selecionar o recurso de notificações push ao fazer isso.
Será solicitado que você envie uma Solicitação de Assinatura de Certificado (CSR). Você pode seguir as instruções no site de ajuda da Apple para criar um CSR no seu Mac. Assim que o CSR for enviado, você poderá baixar o certificado. Clique duas vezes no .cer arquivo para instalá-lo no Acesso ao Chaveiro.
Para obter o certificado de push no formato exigido pelos servidores da Vonage, você precisará exportá-lo. Localize o certificado dos Serviços de VoIP no Acesso ao Chaveiro e clique com o botão direito do mouse para exportá-lo. Nomeie a exportação applecert e selecione .p12 como formato:

Carregue seu certificado de envio
Você fazer o upload seu certificado no Vonage usando o Painel da API. Abra seu aplicativo no painele, em seguida, abra a opção “Ativar notificações notificações" :
Push Upload on the dashboard
Você pode enviar seu certificado .p12 da etapa anterior e adicionar uma senha, se necessário.
A classe ClientManager
Crie um novo arquivo Swift (CMD + N) e nomeie-o como ClientManager. Essa classe irá encapsular o código necessário para fazer a interface com o Client SDK, já que você precisará obter informações do Client SDK em vários pontos nas etapas seguintes:
Substitua ALICE_JWT pelo JWT que você gerou anteriormente; em um ambiente de produção, é aqui que você obteria um JWT do seu servidor de autenticação ou endpoint.
Com essa nova classe, você precisará mover o código de chamada do Client SDK da ViewController classe para a ClientManager classe. As duas classes se comunicarão com os ClientManagerDelegate observadores. Faça as seguintes alterações na sua ViewController classe:
class ViewController: UIViewController {
private let connectionStatusLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
ClientManager.shared.delegate = self
connectionStatusLabel.text = ""
connectionStatusLabel.textAlignment = .center
connectionStatusLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(connectionStatusLabel)
view.addConstraints([
connectionStatusLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
connectionStatusLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
}
extension ViewController: ClientManagerDelegate {
func clientStatusUpdated(_ clientManager: ClientManager, status: String) {
DispatchQueue.main.async {
self.connectionStatusLabel.text = status
}
}
} Inscreva-se para receber notificações push
O próximo passo é registrar um dispositivo para notificações push, a fim de informar à Vonage para qual dispositivo enviar a notificação push para cada usuário. Na ClientManager classe, adicione a pushToken propriedade e as seguintes funções para lidar com o token push do dispositivo:
final class ClientManager: NSObject {
public var pushToken: Data?
weak var delegate: ClientManagerDelegate?
...
func invalidatePushToken(_ completion: (() -> Void)? = nil) {
print("VPush: Invalidate token")
if let deviceId = UserDefaults.standard.object(forKey: Constants.deviceId) as? String {
client.unregisterDeviceTokens(byDeviceId: deviceId) { error in
if error == nil {
self.pushToken = nil
UserDefaults.standard.removeObject(forKey: Constants.pushToken)
UserDefaults.standard.removeObject(forKey: Constants.deviceId)
completion?()
}
}
} else {
completion?()
}
}
private func registerPushIfNeeded(with token: Data) {
shouldRegisterToken(with: token) { shouldRegister in
if shouldRegister {
self.client.registerVoipToken(token, isSandbox: true) { error, deviceId in
if error == nil {
print("VPush: push token registered")
UserDefaults.standard.setValue(token, forKey: Constants.pushToken)
UserDefaults.standard.setValue(deviceId, forKey: Constants.deviceId)
} else {
print("VPush: registration error: \(String(describing: error))")
return
}
}
}
}
}
private func shouldRegisterToken(with token: Data, completion: @escaping (Bool) -> Void) {
let storedToken = UserDefaults.standard.object(forKey: Constants.pushToken) as? Data
if let storedToken = storedToken, storedToken == token {
completion(false)
return
}
invalidatePushToken {
completion(true)
}
}A registerPushIfNeeded função recebe um token e, em seguida, usa a shouldRegisterToken função para verificar se o token já foi registrado. Caso contrário, registerVoipToken no cliente, a notificação push será registrada na Vonage. Na AppDelegate classe, agora você pode se cadastrar para receber notificações push de VoIP. Importe PushKit no início do arquivo:
import PushKitAdicione uma instância local da ClientManager classe:
class AppDelegate: UIResponder, UIApplicationDelegate {
...
private let clientManager = ClientManager.shared
...
}Crie uma nova extensão no final do arquivo que contenha uma função para registrar o dispositivo para notificações push:
extension AppDelegate: PKPushRegistryDelegate {
func registerForVoIPPushes() {
let voipRegistry = PKPushRegistry(queue: nil)
voipRegistry.delegate = self
voipRegistry.desiredPushTypes = [PKPushType.voIP]
}
}Atualize a didFinishLaunchingWithOptions função para chamar a registerForVoIPPushes função e faça login no Client SDK:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
AVAudioSession.sharedInstance().requestRecordPermission { (granted:Bool) in
print("Allow microphone use. Response: \(granted)")
}
registerForVoIPPushes()
clientManager.login()
return true
}Adicione as PKPushRegistryDelegate funções para lidar com o registro de notificações push à extensão:
extension AppDelegate: PKPushRegistryDelegate {
...
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
clientManager.pushToken = pushCredentials.token
}
func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
clientManager.invalidatePushToken(nil)
}
}O token de push é armazenado como uma propriedade na ClientManager classe, já que você deseja registrar o token na Vonage apenas quando o cliente estiver conectado; portanto, edite a login função na ClientManager classe para lidar com isso:
func login(isPushLogin: Bool = false) {
print("VPush: Login - isPush:", isPushLogin)
guard !isActiveCall else { return }
ongoingPushLogin = isPushLogin
getJWT { jwt in
self.client.createSession(jwt) { error, sessionID in
let statusText: String
if error == nil {
statusText = "Connected"
if isPushLogin {
self.handlePushLogin()
} else {
self.handleLogin()
}
} else {
statusText = error!.localizedDescription
}
self.delegate?.clientStatusUpdated(self, status: statusText)
}
}
}
private func handlePushLogin() {
ongoingPushLogin = false
if let storedAction = storedAction {
storedAction()
}
}
private func handleLogin() {
if let token = pushToken {
registerPushIfNeeded(with: token)
}
} Tratar notificações push recebidas
Com o dispositivo registrado, ele agora pode receber notificações push da Vonage. O Client SDK possui funções para verificar se a carga útil de uma notificação push é a esperada e para processá-la. Quando processCallInvitePushData é chamado, ele converte a carga útil em uma chamada que é recebida na didReceiveInviteForCall função do VGVoiceClientDelegate.
Assim como no registro de um token de push, você só deve processar um push recebido quando o Client SDK estiver conectado. Implemente as funções na ClientManager classe, juntamente com uma variável local para armazenar uma notificação push recebida:
final class ClientManager: NSObject {
...
private var ongoingPushLogin = false
private var ongoingPushKitCompletion: () -> Void = { }
private var storedAction: (() -> Void)?
...
func isVonagePush(with userInfo: [AnyHashable : Any]) -> Bool {
VGVoiceClient.vonagePushType(userInfo) == .unknown ? false : true
}
func processPushPayload(with payload: [AnyHashable : Any], pushKitCompletion: @escaping () -> Void) -> String? {
self.ongoingPushKitCompletion = pushKitCompletion
return client.processCallInvitePushData(payload)
}
...
}O PKPushRegistryDelegate possui uma função que é chamada quando há uma notificação recebida, chamada didReceiveIncomingPushWith adicione isso à extensão PKPushRegistryDelegate no AppDelegate.swift arquivo:
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
if clientManager.isVonagePush(with: payload.dictionaryPayload) {
clientManager.login(isPushLogin: true)
_ = clientManager.processPushPayload(with: payload.dictionaryPayload, pushKitCompletion: completion)
}
}Recomenda-se que você faça o login ao receber uma notificação push de VoIP; é por isso que login é chamada aqui. Isso utiliza a lógica da ClientManager classe que armazena informações sobre um push a ser utilizadas após a conclusão do login. A lógica será implementada posteriormente.
Quando seu aplicativo iOS receber uma notificação push de VoIP, você deverá tratá-la usando a CXProvider classe do framework CallKit. Crie um novo arquivo Swift (CMD + N) chamado ProviderDelegate:
import CallKit
import AVFoundation
import VonageClientSDKVoice
final class ProviderDelegate: NSObject {
private let provider: CXProvider
private let callController = CXCallController()
private var activeCall: UUID? = nil
override init() {
provider = CXProvider(configuration: ProviderDelegate.providerConfiguration)
super.init()
provider.setDelegate(self, queue: nil)
}
static var providerConfiguration: CXProviderConfiguration = {
let providerConfiguration = CXProviderConfiguration()
providerConfiguration.maximumCallsPerCallGroup = 1
providerConfiguration.supportedHandleTypes = [.generic, .phoneNumber]
return providerConfiguration
}()
}A callController propriedade é um CXCallController objeto usado pela classe para lidar com as ações do usuário na interface do CallKit. Em seguida, crie uma extensão no final do arquivo para implementar o CXProviderDelegate:
extension ProviderDelegate: CXProviderDelegate {
func providerDidReset(_ provider: CXProvider) {
activeCall = nil
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
ClientManager.shared.answer(activeCall!.uuidString.lowercased()) { error in
if error == nil {
action.fulfill()
} else {
action.fail()
}
}
}
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
hangup(action: action)
}
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
VGVoiceClient.enableAudio(audioSession)
}
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
VGVoiceClient.disableAudio(audioSession)
}
}Quando o CallKit ativa e desativa a sessão de áudio, as funções do delegado ativam e desativam o áudio do Client SDK por meio da sessão de áudio do CallKit.
Quando a interface do CallKit atende a chamada, ela chama a CXAnswerCallAction função delegada. Isso chama a answer função que você implementará em ClientManager em uma etapa futura.
CXEndCallAction é chamada quando a chamada é encerrada a partir da interface do CallKit, que chama a hangup função que você implementará a seguir. Implemente o restante das funções necessárias na ProviderDelegate classe:
final class ProviderDelegate: NSObject {
...
func reportCall(_ callID: String, caller: String, completion: @escaping () -> Void) {
activeCall = UUID(uuidString: callID)
let update = CXCallUpdate()
update.localizedCallerName = caller
provider.reportNewIncomingCall(with: activeCall!, update: update) { error in
if error == nil {
completion()
}
}
}
func didReceiveHangup(_ callID: String) {
let uuid = UUID(uuidString: callID)!
provider.reportCall(with: uuid, endedAt: Date.now, reason: .remoteEnded)
}
func reportFailedCall(_ callID: String) {
let uuid = UUID(uuidString: callID)!
provider.reportCall(with: uuid, endedAt: Date.now, reason: .failed)
}
private func hangup(action: CXEndCallAction) {
if activeCall == nil {
endCallTransaction(action: action)
} else {
ClientManager.shared.reject(activeCall!.uuidString.lowercased()) { error in
if error == nil {
self.endCallTransaction(action: action)
}
}
}
}
private func endCallTransaction(action: CXEndCallAction) {
self.callController.request(CXTransaction(action: action)) { error in
if error == nil {
self.activeCall = nil
action.fulfill()
} else {
action.fail()
}
}
}
}As reportCall chamadas de função reportNewIncomingCall que aciona a interface do usuário do sistema CallKit; as outras funções servem para atualizar ou encerrar chamadas. Agora que a ProviderDelegate classe está pronta, você pode atualizar a ClientManager classe para utilizá-la. Adicione a providerDelegate propriedade ao gerenciador de clientes:
final class ClientManager: NSObject {
...
private let providerDelegate = ProviderDelegate()
...
}Em seguida, implemente o VGVoiceClientDelegate:
extension ClientManager: VGVoiceClientDelegate {
func voiceClient(_ client: VGVoiceClient, didReceiveInviteForCall callId: VGCallId, from caller: String, with type: VGVoiceChannelType) {
print("VPush: Received invite", callId)
providerDelegate.reportCall(callId, caller: caller, completion: ongoingPushKitCompletion)
}
func voiceClient(_ client: VGVoiceClient, didReceiveHangupForCall callId: VGCallId, withQuality callQuality: VGRTCQuality, reason: VGHangupReason) {
print("VPush: Received hangup")
isActiveCall = false
providerDelegate.didReceiveHangup(callId)
}
func voiceClient(_ client: VGVoiceClient, didReceiveInviteCancelForCall callId: String, with reason: VGVoiceInviteCancelReason) {
print("VPush: Received invite cancel")
providerDelegate.reportFailedCall(callId)
}
func client(_ client: VGBaseClient, didReceiveSessionErrorWith reason: VGSessionErrorReason) {
let reasonString: String!
switch reason {
case .tokenExpired:
reasonString = "Expired Token"
case .pingTimeout, .transportClosed:
reasonString = "Network Error"
default:
reasonString = "Unknown"
}
delegate?.clientStatusUpdated(self, status: reasonString)
}
}Depois que o SDK processar a chamada, você receberá um convite para a chamada na didReceiveInviteForCall função delegada, que, por sua vez, informará sobre a chamada. didReceiveHangupForCall e didReceiveInviteCancelForCall também chamarão suas respectivas funções no delegado do provedor. Para completar a ClientManager classe, adicione o answer e reject funções:
func answer(_ callID: String, completion: @escaping (Error?) -> Void) {
let answerAction = {
print("VPush: Answer", callID)
self.isActiveCall = true
self.client.answer(callID, callback: completion)
}
if ongoingPushLogin {
print("VPush: Storing answer")
storedAction = answerAction
} else {
answerAction()
}
}
func reject(_ callID: String, completion: @escaping (Error?) -> Void) {
let rejectAction = {
print("VPush: Reject", callID)
self.isActiveCall = false
self.client.reject(callID, callback: completion)
}
if ongoingPushLogin {
print("VPush: Storing Reject")
storedAction = rejectAction
} else {
rejectAction()
}
}Mais uma vez, só é possível atender ou rejeitar uma chamada após o Client SDK ter feito o login. Ambas as funções utilizam o ongoingPushLogin para verificar se o login foi concluído com sucesso; caso contrário, a ação é armazenada por meio de storedAction. Se você observar a handlePushLogin função, você verá que, quando o login é concluído, ela chama uma ação armazenada, caso exista uma.
Experimente
Compile e execute (CMD + R) o projeto no seu dispositivo iOS (ou no simulador no Xcode 14 e versões posteriores), aceite as permissões de microfone e bloqueie o dispositivo. Em seguida, ligue para o número vinculado ao seu aplicativo Vonage, conforme feito anteriormente. Você verá a chamada recebida diretamente na tela de bloqueio; depois, ao atendê-la, será direcionado para a conhecida tela de chamada do iOS:


Se você verificar o histórico de chamadas no aparelho, também verá essa chamada listada lá.
E agora?
Você pode encontrar o projeto concluído no GitHub. É possível fazer muito mais com o Client SDK e o CallKit; você pode usar o CallKit para chamadas de saída. Saiba mais sobre o Client SDK na Visão geral do Vonage Client SDK e sobre o CallKit em developer.apple.com.
Compartilhar:
Abdul é um Developer Advocate da Vonage. Ele tem experiência profissional na área de produtos de consumo como engenheiro de iOS. Em seu tempo livre, ele gosta de andar de bicicleta, ouvir música e orientar quem está dando os primeiros passos na área de tecnologia