=======描述======= PushKit是苹果在iOS8时引入的一个推送组件,它和传统的推送不同,传统推送在推送时,App是没有被唤醒的,这也就导致用户只能“被动”的接受显示推送内容,这就导致苹果的推送不够灵活,所以在iOS8时,苹果引入了PushKit。但是在ios13开始,Pushkit必须和Callkit同时使用,否则则会出现崩溃的现象。 CallKit的应用因为某些原因无法在国内的App Store上架,这也就导致PushKit在国内并没有真正的用起来。 =======如何申请PushKit证书======= 它的用法和APNs类似,也分为几个步骤: 1. 在苹果后台申请一个Pushkit使用的证书,选择VoIP Services Certificate。 {{:knowledge:voipcert.jpg?nolink&600|}} 2. 选择你要使用的App。 {{:knowledge:create_a_new_certificate.jpg?nolink&600|}} 3. 上传你的签名文件。 {{:knowledge:upload_a_certificate_signing_request.jpg?nolink&600|}} 4. 生成并下载你的证书。此时下载的格式是x.509,后缀是.cer。 {{:knowledge:download_your_certificate_.jpg?nolink&600|}} 5. 将生成的证书双击导入到电脑,此处需要注意,因为这个证书是用刚刚上传的签名文件制作的,所以只在制作签名的电脑里存在私钥,其他电脑直接导入是没有私钥,无法使用的。 (图中可以看到证书项展开后有一个钥匙的标志,表示这个证书有私钥,可以使用) {{:knowledge:cer.jpg?nolink&600|}} 6. 刚刚导入的证书右键导出,格式为p12。导出时需要输入密码,测试导出的证书带有私钥。 =======如何上传证书到环信======= Voip证书本身不区分开发环境与生产环境,但是因为不同环境的苹果服务器地址不同,证书需要在开发环境和生产环境分别上传,使用同一个证书。 上传操作和APNs证书基本一样,**不同的地方在于上传voip证书时,bundleId需要在应用的bundleId后添加.voip**,如应用bundleId为com.easemob.xxx,上传voip证书时填写com.eaasemob.xxx.voip 可以参考文档:[[http://docs-im.easemob.com/im/ios/apns/deploy#%E4%B8%8A%E4%BC%A0%E6%8E%A8%E9%80%81%E8%AF%81%E4%B9%A6%E5%88%B0%E7%8E%AF%E4%BF%A1|上传证书到环信]] 这里需要单独说明,环信是支持上传多个推送证书的,也就是说它和您APNs的证书不冲突,您可以在您的Appkey下上传多个证书。后面会讲如何区分。 =======客户端配置======= =====前提条件===== 1. 环信ios sdk需要使用3.7.1及以上版本; 2. 因为苹果强制pushkit需要和callkit一起使用,所以需要在ios10以上版本; =====配置pushkit===== 在EMOptions中提供了设置PushKit证书名的Api,您需要将您在上传证书时设置的证书名称写在此处。 /*! * \~chinese * iOS特有属性,PushKit证书名称 * * 只能在[EMClient initializeSDKWithOptions:]时设置,不能在程序运行过程中动态修改 * * \~english * Certificate name of Apple PushKit Service * * Can only be set when initializing the SDK with [EMClient initializeSDKWithOptions:], can't be altered in runtime. */ @property (nonatomic, copy) NSString *pushKitCertName; 2. 需要开启Push Notification,并且在background mode中设置voip {{:knowledge:background_mode.jpg?nolink&600|}} 3. 向系统注册Pushkit token 并监听回调,得到Pushkit token后调用环信接口,将PushKit token传给环信sdk PKPushRegistry *pushKit = [[PKPushRegistry alloc] initWithQueue:nil]; pushKit.delegate = self; pushKit.desiredPushTypes = [NSSet setWithObjects:PKPushTypeVoIP, nil]; 回调: #pragma mark - PKPushRegistryDelegate - (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)pushCredentials forType:(PKPushType)type { // 将收到的pushkit token传给环信 [EMClient.sharedClient registerPushKitToken:pushCredentials.token completion:nil]; } - (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(PKPushType)type { NSLog(@"获取pushkit token 失败"); } // 收到pushkit推送时的回调方法 - (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion { } 到此,注册的操作就已经完成。 ===== 配置CallKit ===== iOS13后,苹果为了防止Voip推送被滥用,规定**必须集成CallKit才能使用,否则会造成crash**。CallKit主要用于APP在收到PushKit推送时,将状态信息通知系统,并将用户的接听、拒绝、挂断等操作回传给APP。 CallKit有两个主要的类CXProvider和CXCallController CXProvider可以将一些外来事件通知给系统 CXCallController可以让系统收到App的一些Request,用户的action,内部的事件 App收到PushKit推送时,首先创建展示UI _providerConfiguration = [[CXProviderConfiguration alloc] initWithLocalizedName:@"环信"]; _providerConfiguration.supportsVideo = YES; _providerConfiguration.maximumCallsPerCallGroup = 1; _providerConfiguration.maximumCallGroups = 1; // CXHandleTypePhoneNumber, CXHandleTypeGeneric, CXHandleTypeEmailAddress _providerConfiguration.supportedHandleTypes = [[NSSet alloc] initWithObjects:[NSNumber numberWithInt:CXHandleTypeGeneric], nil]; UIImage* iconMaskImage = [UIImage imageNamed:@"AppIcon"]; _providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation(iconMaskImage); self.provider = [[CXProvider alloc] initWithConfiguration:self.providerConfiguration]; [self.provider setDelegate:self queue:dispatch_get_main_queue()]; _callController = [[CXCallController alloc] initWithQueue:dispatch_get_main_queue()]; 然后展示来电页面 [self.provider reportNewIncomingCallWithUUID:self.currentCall update:update completion:^(NSError * _Nullable error) { }]; 当用户在来电页面进行接听、拒绝或挂断等操作时,系统通过CXProvider的Delegate协议方法告知APP - (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action { [action fulfill]; } - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action { publicIsAnswer = YES; _answerAction = action; } - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action { [action fulfill]; } =====发送使用pushkit的消息===== 目前是通过您在发消息时设置不同的扩展来实现的,格式: { "em_push_ext":{ "type":"call", "custom":{ "xxx":"xxx", "yyy":"yyy" } } } 其中custom里的内容可以自己定义。 示例 + (void)sendPushKitCallMessageToUser:(NSString *)aUsername myNickname:(NSString *)aNickname { NSString *currentUsername = EMClient.sharedClient.currentUsername; NSString *nickName = aNickname; if (nickName && nickName.length > 0) { // 如果传过来的nickname是空,则使用当前登录的环信id nickName = currentUsername; } // 构造消息并发送 EMTextMessageBody *body = [[EMTextMessageBody alloc] initWithText:[nickName stringByAppendingString:@"邀请您进行语音通话"]]; EMMessage *msg = [[EMMessage alloc] initWithConversationID:aUsername from:currentUsername to:aUsername body:body ext:nil]; /** ext中,关键字和格式固定,需要为如下格式,其中type必须为call { @{@"em_push_ext" : @{ @"type":@"call", @"custom":@{ @"xxx":@"xxxx" } } }; } */ msg.ext = ({ @{@"em_push_ext" : @{ @"type":@"call", @"custom":@{ @"nickname": nickName ?: @"", @"caller":currentUsername, @"action":@"call", } } }; }); [EMClient.sharedClient.chatManager sendMessage:msg progress:nil completion:nil]; } =====如何接收===== 当您完成配置pushkit后,您的pushkit token和证书名就已经在环信后台配置,此时如果您不在线,别人又发送了上面配置过ext的消息,您将收到一个pushkit推送,并收到回调。 示例如下: - (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion { NSDictionary *pushInfo = payload.dictionaryPayload; /* e中的内容为发送方填入ext:custom中的内容 { "m": "772299818058910052", "t": "du001", "aps": { "badge": 2, "alert": { "body": "du002邀请您进行语音通话" }, "sound": "default" }, "e": { "xxx": "xxx", "yyy": "yyy" }, "f": "du002" } */ } e字段中json对应的是在发送时设置的custom中的字段。其他字段的含义,可以参考文档[[http://docs-im.easemob.com/im/ios/apns/content#apns_%E5%86%85%E5%AE%B9%E8%A7%A3%E6%9E%90|推送字段解析]] =======环信如何区分多个证书======= 因为你传了两个证书,每个证书都有自己的名字,当您发送消息时,ext中设置的type:call,就是告诉服务器这条消息要走pushkit,此时对方不在线,环信服务器就会优先去查看接收方是否设置了puskit的证书,如果有证书,同时又有pushkit用的token,服务器就会根据这个信息向苹果pushkit的接口发请求,并带上消息和ext中其他的信息。如果消息的ext中没有带call或者相关的字段,则只会发送使用APNs的证书和token去发APNs。 **注意** 因为苹果强制要求pushkit和callkit一起使用,否则会导致pushkit无法使用,针对这个情况,我们提供了一个[[https://github.com/dujiepeng/EaseMobCallKit|Callkit demo]]供您参考,另外,当您想使用pushkit的时候,请一定要确认您的app是否需要上架和上架地区是哪里。如果您是想在国内上架,建议您使用[[http://docs-im.easemob.com/rtc/one2one/ios#%E7%A6%BB%E7%BA%BF%E5%8F%91%E6%8E%A8%E9%80%81%E5%B9%B6%E8%AE%BE%E7%BD%AE%E9%93%83%E5%A3%B0|自定义铃声]]或者使用[[https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification|Notification Service Extension]]的方式来达到唤醒,如果您使用这种方式,需要向ext中添加"em_push_mutable_content"字段,具体可以参考文档[[http://docs-im.easemob.com/im/ios/apns/content#%E5%90%91apns%E4%B8%AD%E6%B7%BB%E5%8A%A0%E6%89%A9%E5%B1%95%E5%AD%97%E6%AE%B5|向apns中添加扩展字段]]。