前言:如果要求使用这两种库,请在查询资料并自己尝试后,多参考苹果官方的API文档:
PushKit:https://developer.apple.com/documentation/pushkit?language=objc
CallKit:https://developer.apple.com/documentation/callkit?language=objc
一、PushKit
一、简介
1.iOS10之后,苹果推出了CallKit框架增强的VoIP应用的体验,主要表现在3个方面:
在锁屏状态下,如果有网络电话呼入,VoIP的应用可以打开系统电话应用的待接听界面。
VoIP的应用内发起通话,挂断电话等记录可以体现在系统电话应用的通话记录中。
从系统电话应用的通话记录,通讯录或者Siri的进入VoIP的应用,发起通话。
二、使用(主要代码)
1.注册token
PKPushRegistry *voipRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
voipRegistry.delegate = self;
voipRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
2.获取token,并将token上传到自己的服务器
- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type {
NSData *deviceToken = credentials.token;
NSString *token = @"";
if (@available(iOS 13.0, *)) {
const unsigned char *dataBuffer = (const unsigned char *)deviceToken.bytes;
NSMutableString *myToken = [NSMutableString stringWithCapacity:(deviceToken.length * 2)];
for (int i = 0; i < deviceToken.length; i++) {
[myToken appendFormat:@"%02x", dataBuffer[i]];
}
token = (NSString *)[myToken copy];
} else {
NSCharacterSet *characterSet = [NSCharacterSet characterSetWithCharactersInString:@"<>"];
NSString *myToken = [[deviceToken description] stringByTrimmingCharactersInSet:characterSet];
token = [myToken stringByReplacingOccurrencesOfString:@" " withString:@""];
}
NSLog(@"didUpdatePushCredentials token = %@", token);
[[PushNotificationManager shareInstance] setupPushKitToken:token];//保存token到自己的服务器
}
3.获取推送消息,校验并弹出系统电话界面
/// iOS8.0-iOS11.0
- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type {
[self didReceiveIncomingPushWithPayload:payload withCompletionHandler:^{}];
}
/// iOS11.0+
- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type withCompletionHandler:(nonnull void (^)(void))completion {
[self didReceiveIncomingPushWithPayload:payload withCompletionHandler:completion];
}
- (void)didReceiveIncomingPushWithPayload:(PKPushPayload *)payload withCompletionHandler:(nonnull void (^)(void))completion{
//是否登陆-一般voip注册的token会绑定到用户
if (![SecurityProvider isUserLogined]) {
return;
}
//从字典payload.dictionaryPayload中获取推送来的数据,自己解析做逻辑处理,
//以下代码可以当作参考,不用深究,基本思路是对推送数据的处理:
//1.当前没有任何来电时,收到推送解析数据后直接展示系统电话界面
//2.当前有来电未接听时,收到推送解析数据后,更新数据,保证接听时数据为最新的推送数据
//3.当前有来电已接听时,1.不同用户打来的,忙线处理(告诉服务端当前用户忙线)2.同一个用户打来的直接进行通话。
NSString *action = [payload.dictionaryPayload objectForKey:@"action"];
if([action isEqualToString:@"5010"]){
NSString * dataString= [payload.dictionaryPayload objectForKey:@"data"];
NSDictionary * extraDic = [[ProviderDelegate shareInstance] dictionaryWithJsonString:dataString];
NSString *hospital = [payload.dictionaryPayload objectForKey:@"hospital"];
hospital = hospital ==nil?@"":hospital;//hospital为nil崩溃
NSString *topic = [extraDic objectForKey:@"topic"];
NSString *password = [extraDic objectForKey:@"password"];
NSString *token = [extraDic objectForKey:@"token"];
NSString *bizNo = [extraDic objectForKey:@"bizNo"];
NSString *sessionId = [extraDic objectForKey:@"sessionId"];
NSString *topicTime = [extraDic objectForKey:@"topicTime"];
NSString *pushType = [payload.dictionaryPayload objectForKey:@"pushType"];
if ([pushType isEqualToString:@"start"]) {
//检查是否已有通话
if ([[ProviderDelegate shareInstance] checkHadIncomingCall]) {
NSString *bizNo_old = [[NSUserDefaults standardUserDefaults] valueForKey:@"bizNo"];
//忙线
//不同用户拨打
if (![bizNo_old isEqualToString:bizNo]) {
[[ZoomViewManager shareInstance] busyVideoCallWithSessionId:sessionId bizNO:bizNo];
return;;
}else{
//已经接通的情况-同一个医生特殊情况打多次-不再显示电话页面,直接进入zoom
//topicTime>topicTime_old:同一个医生多次拨打,用户收到的通知顺序按照时间来判断
if ([topicTime longLongValue]>[self.topicTime_old longLongValue]) {
if ([[ProviderDelegate shareInstance] checkHadCallActive]) {//是否正在通话
[[ZoomViewManager shareInstance] leaveVideoCall];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[ZoomViewManager shareInstance] joinOnlyWithSesscionName:topic
userName:hospital
sessionPassword:password
token:token
bizNo:bizNo
sessionId:sessionId];
});
}
}else{
return;
}
}
}
if ([topicTime longLongValue]>[self.topicTime_old longLongValue]) {
[[NSUserDefaults standardUserDefaults] setValue:pushType forKey:@"pushType"];
[[NSUserDefaults standardUserDefaults] setValue:hospital forKey:@"hospital"];
[[NSUserDefaults standardUserDefaults] setValue:topic forKey:@"topic"];
[[NSUserDefaults standardUserDefaults] setValue:password forKey:@"password"];
[[NSUserDefaults standardUserDefaults] setValue:token forKey:@"token"];
[[NSUserDefaults standardUserDefaults] setValue:bizNo forKey:@"bizNo"];
[[NSUserDefaults standardUserDefaults] setValue:sessionId forKey:@"sessionId"];
self.topicTime_old = topicTime;
}
//没有通话可以展示系统通话界面
if (![[ProviderDelegate shareInstance] checkHadIncomingCall]) {
[[ProviderDelegate shareInstance] reportIncomingCallHandle:hospital withCompletionHandler:completion];
}
}
}
}
注意事项:
1.官网介绍此方法的实现必须通过调用应用对象的方法向CallKit框架报告类型通知。否则在 iOS 13.0 及更高版本上,如果未能向 CallKit 报告调用,系统将终止应用,不再给应用发送推送(应用在前台还可以继续收到)。
2.// iOS11.0+ withCompletionHandler:(nonnull void (^)(void))completion,其中completion必须在callkit唤起系统电话界面完成时候调用,否则会导致部分机型间断的收到消息推送(必须发两次消息推送才能收到消息)
三、踩坑经验
1.keychain问题:在杀死app/杀死app并锁屏状态,keyChain有保护机制,所以此时收到推送消息,去读取keyChain数据时会导致读取失败,影响app正常流程。
Locked home screens. The keychain tutorials always left the accessibility settings for the keychain blank,
so it would default to Apple's lowest/safest access level.
This level however doesn't allow keychain access if the user has a passcode on the lock screen. Bingo!
This explains the sporadic behavior and why this only happens to a small percentage of users.
二、CallKit
一、简介
CallKit 是一个iOS10新框架,用于改善 VoIP 的体验,允许 App 和原生的 Phone UI 紧密集成,你的 App 将能够:
调用原生的呼入界面,无论锁屏/不锁屏状态。
从原生电话 App 的通讯录、个人收藏、最近通话中发起通话。
通过系统服务监听来电和去电。
用电话通讯录识别或拦截来电。
callkit并不能直接通话/视频,需要配合语音/视频库进行通信(如:zoom),
杀死app/锁屏都会执行CallKit代码,但一些网络请求等会失败
二、使用(主要代码)
1.初始化一个CXProvider,定义成单利
- (instancetype)init {
self = [super init];
if (self) {
CallManager *manager = [CallManager shareInstance];
self.callManager = manager;
CXProvider *provider = [[CXProvider alloc]initWithConfiguration:[self providerConfiguration]];
/*
*设置代理可以接收到系统电话界面上的所有操作回调
*例外:点击视频图标无回调,自动打开app(暂无用没深究)
*/
[provider setDelegate:self queue:nil];
self.provider = provider;
}
return self;
}
/*
*初始定义本地电话页面显示效果
*接到消息推送,唤起电话页面前可以更新该配置CXCallUpdate
*详解见官网CXProviderConfiguration
*/
- (CXProviderConfiguration *)providerConfiguration{
CXProviderConfiguration *providerConfiguration = [[CXProviderConfiguration alloc]initWithLocalizedName:@"HELPO"];
providerConfiguration.supportsVideo = YES;
providerConfiguration.maximumCallsPerCallGroup = 1;
providerConfiguration.maximumCallGroups = 1;
providerConfiguration.supportedHandleTypes = [NSSet setWithObject:@(CXHandleTypePhoneNumber)];
return providerConfiguration;
}
2.弹出系统电话界面
- (void)reportIncomingCallHandle:(NSString *)handle withCompletionHandler:(nonnull void (^)(void))completion{
if (!self.isHadReportIncomingCall) {
self.isHadReportIncomingCall = YES;
NSUUID *uuid = [NSUUID UUID];
self.uuid = uuid;
CXCallUpdate *update = [[CXCallUpdate alloc]init];
update.remoteHandle = [[CXHandle alloc]initWithType:CXHandleTypePhoneNumber value:handle];
update.localizedCallerName =handle;
update.supportsHolding = NO; //通话过程中再来电,是否支持保留并接听
update.supportsGrouping = NO; //通话是否可以加入一个群组
update.supportsDTMF = NO; //是否支持键盘拨号
update.hasVideo = YES;//本次通话是否有视频
[self.provider reportNewIncomingCallWithUUID:uuid update:update completion:^(NSError * _Nullable error) {
Call *call = [[Call alloc]initWith:uuid isOutGoing:NO handle:handle];
[self.callManager addCall:call];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(60 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
for (Call *call in self.callManager.calls) {
if (call.callState == CallStateConnecting && call.uuid == uuid) {
call.isAutoEnd = YES;
[self stopCalling];//倒计时3分钟,用户还未接听,代码挂断
}
}
});
completion();
}];
}
}
注意事项:
1.pushkit有提到,completion必须在reportNewIncomingCallWithUUID完成之后调用
3.点击接通代理回调
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action{
Call *call = [self.callManager callWithUUID:action.callUUID];
if (!call) {
[action fail];
self.isHadReportIncomingCall = NO;
}
[Audio configureAudioSession];
[call answer];//使用的zoom,这里接通zoom
[action fulfill];
}
注意事项:
1.接听成功要执行fulfill,否则执行fail,更新系统电话界面
4.点击挂断代理回调
- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action{
Call *call = [self.callManager callWithUUID:action.callUUID];
if (!call) {
[action fail];
}
[Audio stopAudio];
if(call.callState == CallStateActive){
//接听后挂断callkit
[[ZoomViewManager shareInstance] leaveVideoCallAndCallKit];
} else if(call.callState == CallStateConnecting) {
NSString *pushType = [[NSUserDefaults standardUserDefaults] valueForKey:@"pushType"];
if ([pushType isEqualToString:@"start"] && [JKNSecurityProvider isUserLogined]){
NSString *bizNo = [[NSUserDefaults standardUserDefaults] valueForKey:@"bizNo"];
NSString *sessionId = [[NSUserDefaults standardUserDefaults] valueForKey:@"sessionId"];
if (call.isAutoEnd) {
call.isAutoEnd = false;
//未接听-三分钟自动后自动挂断
[[ZoomViewManager shareInstance] noAnswerVideoCallWithSessionId:sessionId bizNO:bizNo];
}else{
//未接听挂断callkit,用户主动挂断
[[ZoomViewManager shareInstance] rejectVideoCallWithSessionId:sessionId bizNO:bizNo];
}
}
}
self.isHadReportIncomingCall = NO;
self.uuid = nil;
[call end];
[action fulfill];
[self.callManager removeCall:call];
}