描述

PushKit是苹果在iOS8时引入的一个推送组件,它和传统的推送不同,传统推送在推送时,App是没有被唤醒的,这也就导致用户只能“被动”的接受显示推送内容,这就导致苹果的推送不够灵活,所以在iOS8时,苹果引入了PushKit。但是在ios13开始,Pushkit必须和Callkit同时使用,否则则会出现崩溃的现象。 CallKit的应用因为某些原因无法在国内的App Store上架,这也就导致PushKit在国内并没有真正的用起来。

如何申请PushKit证书

它的用法和APNs类似,也分为几个步骤:

1. 在苹果后台申请一个Pushkit使用的证书,选择VoIP Services Certificate。

2. 选择你要使用的App。

3. 上传你的签名文件。

4. 生成并下载你的证书。此时下载的格式是x.509,后缀是.cer。

5. 将生成的证书双击导入到电脑,此处需要注意,因为这个证书是用刚刚上传的签名文件制作的,所以只在制作签名的电脑里存在私钥,其他电脑直接导入是没有私钥,无法使用的。 (图中可以看到证书项展开后有一个钥匙的标志,表示这个证书有私钥,可以使用)

6. 刚刚导入的证书右键导出,格式为p12。导出时需要输入密码,测试导出的证书带有私钥。

如何上传证书到环信

Voip证书本身不区分开发环境与生产环境,但是因为不同环境的苹果服务器地址不同,证书需要在开发环境和生产环境分别上传,使用同一个证书。

上传操作和APNs证书基本一样,不同的地方在于上传voip证书时,bundleId需要在应用的bundleId后添加.voip,如应用bundleId为com.easemob.xxx,上传voip证书时填写com.eaasemob.xxx.voip 可以参考文档:上传证书到环信

这里需要单独说明,环信是支持上传多个推送证书的,也就是说它和您APNs的证书不冲突,您可以在您的Appkey下上传多个证书。后面会讲如何区分。

客户端配置

1. 环信ios sdk需要使用3.7.1及以上版本; 2. 因为苹果强制pushkit需要和callkit一起使用,所以需要在ios10以上版本;

在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

3. 向系统注册Pushkit token 并监听<PKPushRegistryDelegate>回调,得到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
{
}

到此,注册的操作就已经完成。

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];
}

目前是通过您在发消息时设置不同的扩展来实现的,格式:

{
  "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中的字段。其他字段的含义,可以参考文档推送字段解析

环信如何区分多个证书

因为你传了两个证书,每个证书都有自己的名字,当您发送消息时,ext中设置的type:call,就是告诉服务器这条消息要走pushkit,此时对方不在线,环信服务器就会优先去查看接收方是否设置了puskit的证书,如果有证书,同时又有pushkit用的token,服务器就会根据这个信息向苹果pushkit的接口发请求,并带上消息和ext中其他的信息。如果消息的ext中没有带call或者相关的字段,则只会发送使用APNs的证书和token去发APNs。

注意 因为苹果强制要求pushkit和callkit一起使用,否则会导致pushkit无法使用,针对这个情况,我们提供了一个Callkit demo供您参考,另外,当您想使用pushkit的时候,请一定要确认您的app是否需要上架和上架地区是哪里。如果您是想在国内上架,建议您使用自定义铃声或者使用Notification Service Extension的方式来达到唤醒,如果您使用这种方式,需要向ext中添加“em_push_mutable_content”字段,具体可以参考文档向apns中添加扩展字段