0 推送的分类
在介绍方案之前,先简单过一下push的类型:

参考:sending-notification-requests-to-apns?language=objc
远程推送的分类很多,设备接受到一条推送消息后,如何分发,取决于消息头,以及payload的内容。在这些当中,符合我们通过push模拟来电的,仅有voip(采用PushKit+CallKit,参考如流),以及alert(参考微信),而由于政策原因,我们只能选取后者。
一些参考文献
-
push to talk:
https://developer.apple.com/documentation/pushtotalk?language=objc
https://developer.apple.com/documentation/pushtotalk/creating-a-push-to-talk-app?language=objc -
fileprovider
https://developer.apple.com/documentation/pushkit/pkpushtype/fileprovider?language=objc
1 alert消息处理流程回顾
鉴于Voip(PushKit + CallKit)方案审核被拒,现设计微信的语音来电方案。经过调用,远程推送中的alert模式的推送,符合我们的场景(其他模式参考:),在方案设计前,先回顾一下一条alert推送消息的流转过程:

在这个过程中,有三点需要注意:
- 每条push最大的处理时间约为30s
- 若主动调用Complete推出横幅后,最多还有5s的时间处理其他事物
- 超过30s后,会触发系统函数
-serviceExtensionTimeWillExpire,该函数会隐式调用Complete,推出横幅。
2 设计目标及方案
我们的设计目标如下:
- APP收到【语音电话类型】的推送消息后,设备顶部展示横幅,并持续震动&响铃(若开了静音则无法响铃)
- 震动&响铃期间,锁屏&通知中心显示来电消息
- APP进前台,停止震动&响铃,唤起端内来电弹窗。并移除锁屏&通知中心的通知
- 超时未接听,停止震动&响铃,并移除锁屏&通知中心的通知(可选)
- 推送期间,服务端终止呼叫,设备停止震动&响铃,并移除锁屏&通知中心的通知(可选)
下面针对目标,分别进行方案设计:
目标1:收到推送后,持续震动、响铃
首先,后端推的push消息结构中,一定要有一个字段,用于标识来电,结合目标5,我们可以设计结构如下(alert类型支持的完整参数:https://developer.apple.com/documentation/usernotifications/generating-a-remote-notification?language=objc):
{
"pushType": 1, // 1为来电 2为挂断
"aps":
{
"alert":
{
"title": "推送通知标题",
"subtitle": "推送通知副标题",
"body": "这是推送通知的正文内容"//最多4行字
},
"mutable-content": 1 //这里必须设置为1
}
}
静态效果如图:


当收到推送后,若pushType为1时,就是我们处理来电语音的时机。由于消息体中mutabe-content字段的设置,使得Push Service插件能够收到消息进行处理,即执行了-didReceiveNotificationRequest:withContentHandler:方法
在这个方法中我们可以推出横幅,然后在新线程循环播放震动&铃声,同时挂起当前线程,防止由于横幅被推出后,整个流程只能执行5s:
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
NSDictionary *pushDictionary = request.content.userInfo;
__weak typeof(self) weakSelf = self;
if ([pushDictionary.allKeys containsObject:@"pushType"]) {
[[DXMPushPhoneManager shareManager] callingWithRequest:request complete:^(UNNotificationContent * _Nonnull contentToDeliver) {
weakSelf.contentHandler(contentToDeliver);
}];
}
}
这里我们看下在Push Service进程中的默认线程 endpoint:

而我们的震动&响铃任务执行在主线程:

可以确认挂起endpoint线程,不会影响震动&响铃的任务
然后我们还要做一些辅助性的事务:
- 在沙盒中存储来电push的消息。(注意使用appgroup跨进程沙盒存储)
目标2 震动&响铃期间,锁屏&通知中心显示来电消息
由于在上一步中,在启动震动&铃声循环任务后,就主动推出了横幅,故本目标可以满足
目标3 APP启动/回到前台的处理
1.停掉震动&响铃: 当APP进入前台后,沙盒更新对应标记位(注意使用appgroup跨进程沙盒存储),Push Service进程中,震动&铃声定时器每次循环都根据标记位(注意使用appgroup跨进程读取沙盒),判断是否停止循环。因此本目标达成。
- 在目标1的方案中,我们在沙盒中存储了push的消息,那么无论是用户点击桌面图标,还是点击Push将APP拉起,我们都只需要从沙盒中读取push的数据即可(为统一处理,放弃掉对scheme拉起的处理)
目标4 超时处理
用户超时未点击推送条,或主动打开APP,导致触发-serviceExtensionTimeWillExpire函数,那么在这个时机:
- 起一条本地push,提示用户未接听(可选)
- 调用
[[UNUserNotificationCenter currentNotificationCenter] removeDeliveredNotificationsWithIdentifiers:identifers];清楚掉通知栏中的通知(可选) - 清楚沙盒中的push信息
目标5 中断呼叫处理
后端下发一条 pushType为2的push消息,端上在处理时:
- 停掉震动&铃声循环任务
- 调用
[[UNUserNotificationCenter currentNotificationCenter] removeDeliveredNotificationsWithIdentifiers:identifers];清除掉通知栏中的通知 - 移除沙盒中的push消息
- 推出挂断推送横幅,播放挂断音效,清除掉通知栏中的通知(可选)
完整的Demo效果
-
未接听
-
接听
3.终止呼叫
3 还能做哪些事情?
3.1 定制化Push通知完整页面(iOS10+)
当用户长按push条时,苹果提供了展示一个完整push内容页面的能力(不需要启动app),
这个需要我们引入 Notification Content Extension的能力:
步骤 1:创建通知内容扩展目标
在 Xcode 中,选择你的项目文件。
点击左下角的加号按钮,选择 “New Target”。
在模板选择界面,选择 “Notification Content Extension”。
点击 “Next”,然后填写扩展的名称和其他信息。
点击 “Finish”。
Xcode 会为你创建一个新的扩展,并包含一些示例代码。

步骤 2:配置扩展的 Info.plist
打开新创建的扩展目标中的 Info.plist 文件。
确保 NSExtension 字典中包含以下键值对:(UNNotificationExtensionDefaultContentHidden 控制的是是否在头部展示推送条的内容)
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>UNNotificationExtensionCategory</key>
<string>your_category_identifier</string>
<key>UNNotificationExtensionInitialContentSizeRatio</key>
<real>0.5</real>
<key>UNNotificationExtensionDefaultContentHidden</key>
<true/>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.content-extension</string>
</dict>
步骤 3:实现扩展的主界面
在扩展目标中,Xcode 会生成一个包含 NotificationViewController示例视图控制器。这个视图控制器需要遵循 UNNotificationContentExtension 协议。我随便搞了下布局
步骤4:注册。注意这里的your_category_identifier要和plist中保持一致。
- (void)registerNotificationCategories {
UNNotificationCategory *category = [UNNotificationCategory categoryWithIdentifier:@"your_category_identifier"
actions:@[]
intentIdentifiers:@[]
options:UNNotificationCategoryOptionCustomDismissAction];
[[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:[NSSet setWithObject:category]];
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self registerNotificationCategories];
return YES;
}
步骤5:发送通知。要注意通知中的category字段要对应:
{
"aps": {
"alert": {
"title": "紧急通知",
"subtitle": "请立即查看",
"body": "这是一个重要的推送通知内容。",
},
"sound": "default",
"badge": 1,
"category": "your_category_identifier",
"mutable-content": 1,
"content-available": 1
}
}
3.2 定制化Push通知快捷按钮
这个其实和3.1一样,也是借助于Notification Content Extension的能力,只需要在注册的时候,加上按钮即可:
- (void)registerNotificationCategories {
// 定义回复操作
UNTextInputNotificationAction *replyAction = [UNTextInputNotificationAction actionWithIdentifier:@"REPLY_ACTION"
title:@"回复"
options:UNNotificationActionOptionAuthenticationRequired
textInputButtonTitle:@"Send"
textInputPlaceholder:@"Type your message"];
// 定义查看对话操作
UNNotificationAction *viewAction = [UNNotificationAction actionWithIdentifier:@"VIEW_ACTION"
title:@"查看"
options:UNNotificationActionOptionForeground];
UNNotificationCategory *category = [UNNotificationCategory categoryWithIdentifier:@"hexiaoyang_category_identifier"
actions:@[replyAction, viewAction]
intentIdentifiers:@[]
options:UNNotificationCategoryOptionCustomDismissAction];
[[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:[NSSet setWithObject:category]];
}
效果:
自定义按压界面参考 customizing-the-appearance-of-notifications?language=objc
3.3 定制化左侧logo(iOS15+)
这个默认是app的icon,那么如果想换,需要额外借助 Communication Notification 以及 Siri Intent的能力。
这里有篇文章写的很细,7448153 不过多赘述
4 Q&A
Q: push通知条的样式能够定制吗?
A:不能定制,最多展示左侧logo、主标题、副标题、和最多4行内容区。右侧可以展示附件的缩略图。
另外可以定制长按Push条后展示的一个自定义的页面,参考3.1节。也可以定制长按后的快捷按钮,参考3.2节
Q: push本身的铃声,会和push来电冲突吗?
A: 如果通知内容中含有 mutable-content:1字段,这意味着端上可以定制化处理,以本场景为例,只需要在推出横幅前,删掉横幅数据中的sound字段:
//清空声音
self.bestAttemptContent.sound = nil;
//推出横幅
self.contentHandler(self.bestAttemptContent);
Q:设置中,推送展示位都关了,还能震动/响铃吗
A: iOS17经测试是不能的,当三个展示位都关闭后,上级菜单就显示通知关了。需要再看看别的系统
Q: 还存在哪些棘手的问题
A: 勿扰模式阻挡不住震动&响铃! 对于推送横幅本身的声音,勿扰模式是可以进行阻挡的,但这个方案中的震动、响铃走的是主线程。且系统没有暴露出api来判断当前设备的模式。看了下微信语音来电,也有这个问题...