PushKit/Callkit使用经验

前言:如果要求使用这两种库,请在查询资料并自己尝试后,多参考苹果官方的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];
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值