这是我参与11月更文挑战的第4天,活动详情查看:​​2021最后一次更文挑战​​。

引言

粉丝福利:搜索​​#小程序:iOS逆向 ​​​,关注​​公众号:iOS逆向​​领取福利【掘金小册5折优惠码】

一年一度的iOS 系统 API适配来了,9 月 14 日起 App Store Connect 已经开放 iOS 15 和 iPadOS 15 App 的提交,同时苹果宣布​​自 2022 年 4 月起,所有提交至 App Store 的 iOS 和 iPadOS app 都必须使用 Xcode 13 和 iOS 15 SDK 构建。​


Xcode 13 正式版包含 iOS 15,iPadOS 15,tvOS 15,watchOS 8 以及 macOS Big Sur 11.3 SDK。​​Xcode 13 需在 macOS 11.3 及以上版本运行​​,支持 iOS 9,tvOS 9,watchOS 2 及以上系统设备调试;

也正式支持了 Vim。

Xcode 13 Release Notes: ​​developer.apple.com/documentati…​​ iOS15适配本地通知功能及语音播报探索_iOS


I 消息推送

语音播报在iOS15之前的实现思路:

  1. 将你想要播放的音频拆分,放到主程序的包里
  2. 利用Service Extension,在收到服务端的推送的时候,按照顺序发送本地通知
  3. 本地通知的sound就是对应的音频拆分
  4. 将收到的推送的sound设置为nil,避免打断本地推送的语音播报。
  5. 设置队列处理消息推送,避免短时间内收到多个推送导致的问题。
  6. 设置本地通知不弹出横幅(iOS15失效了无法设置)


title和body都设置为空,或者注册通知的时候不启用UNAuthorizationOptionAlert,还要注意的是拓展里面需要设置一下本地化。 但是在iOS15如果body为空,将无法播放声音。


<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>

1.1 本地推送适配

推送新特性: iOS15以上的新增属性 interruptionLevel为枚​​UNNotificationInterruptionLevel​

需求:利用本地推送实现消息的语音播报(在扩展里面发本地通知,最后的接收方是主程序)


扩展在收到通知之后 -> 合成音频 -> 存储到扩展的对应路径 -> 扩展自己给自己发一个本地通知那个通知的sound设置成合成文件


iOS15之前的实现思路:

  1. 将你想要播放的音频拆分,放到主程序的包里
  2. 利用Service Extension,在收到服务端的推送的时候,按照顺序发送本地通知
  3. 本地通知的sound就是对应的音频拆分

问题:iOS12.1之后利用本地推送实现消息的语音播报,在iOS15 没有声音。​​kunnan.blog.csdn.net/article/det…​​

原因: iOS15本地推送新增了中断级别属性 ​​interruptionLevel​​,对通知进行了分级 。而且通知的内容不能为空。

方案:使用非Passive的中断级别进行本地通知才会有声音,且本地推送一定要有内容,即body不能为空。​​content.body = @" 不能为空";​


iOS15适配本地通知功能及语音播报探索_本地通知_02


UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = @"";
content.subtitle = @"";
content.sound = [UNNotificationSound soundNamed:fileName];
content.badge = @(1);

if (@available(iOS 15.0, *)) {
content.interruptionLevel = UNNotificationInterruptionLevelTimeSensitive;//会使手机亮屏且会播放声音;可能会在免打扰模式(焦点模式)下展示
// @"{\"aps\":{\"interruption-level\":\"time-sensitive\"}}";
// @"{\"aps\":{\"interruption-level\":\"active\"}}";
content.body = @" ";// 本地推送一定要有内容,即body不能为空。

}else{

content.body = @"";

}

UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.001 repeats:NO];
// 添加通知的标识符,可以用于移除,更新等操作
NSString *identifier = [NSString stringWithFormat:@"localPushId%lld", (long long)[[NSDate date] timeIntervalSince1970]];
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger];
[center addNotificationRequest:request withCompletionHandler:^(NSError *_Nullable error) {
CGFloat waitTime = [self timeForAudioFileWithFileName:fileName];
// CGFloat waitTime = 0.3;

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(waitTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self localNotificationPushNextFile];
});
}];

中断级别目前分为四种:

typedef NS_ENUM(NSUInteger, UNNotificationInterruptionLevel) {
// Added to the notification list; does not light up screen or play sound
UNNotificationInterruptionLevelPassive,

// Presented immediately; Lights up screen and may play a sound
UNNotificationInterruptionLevelActive,

// Presented immediately; Lights up screen and may play a sound; May be presented during Do Not Disturb
UNNotificationInterruptionLevelTimeSensitive,

// Presented immediately; Lights up screen and plays sound; Always presented during Do Not Disturb; Bypasses mute switch; Includes default critical alert sound if no sound provided
UNNotificationInterruptionLevelCritical,

} API_AVAILABLE(macos(12.0), ios(15.0), watchos(8.0), tvos(15.0));
  1. Passive:被动类型的通知不会使手机亮屏并且不会播放声音。
  2. Active: 活动类型的通知会使手机亮屏且会播放声音,为默认类型。
  3. Time Sensitive(时间敏感):会使手机亮屏且会播放声音;可能会在免打扰模式(焦点模式)下展示。


设置推送通知数据: 时间敏感的中断级别可以使用“interruption-level” payload key:​​{"aps":{"interruption-level":"time-sensitive"}}​

时效性通知开发者无法直接使用,需要配置对应的权限: a. xcode 开启对应能力

iOS15适配本地通知功能及语音播报探索_ios_03

b. 开发者后台配置appID支持该权限(通过Xcode开启对应能力通常会自动添加)


iOS15适配本地通知功能及语音播报探索_ios_04

  1. Critical(关键):会立刻展示,亮屏,播放声音,无效免打扰模式,并且能够绕过静音,如果没有设置声音则会使用一种默认的声音。


适用于地震等紧急情况,需要特殊申请。


判断是否有时间敏感权限 ​​@property(readonly, nonatomic) UNNotificationSetting timeSensitiveSetting;​​,如果没有需要提示用户开启。

UNNotificationSetting

typedef NS_ENUM(NSInteger, UNNotificationSetting) {
// The application does not support this notification type
UNNotificationSettingNotSupported = 0,

// The notification setting is turned off.
UNNotificationSettingDisabled,

// The notification setting is turned on.
UNNotificationSettingEnabled,
} API_AVAILABLE(macos(10.14), ios(10.0), watchos(3.0), tvos(10.0));

1.2 测试

开发者想打ad hot 的话,需要能访问云端管理的分发证书。 iOS15适配本地通知功能及语音播报探索_ios_05

可以使用极光的网页端或者接口进行测试,或者使用smart push。


iOS15适配本地通知功能及语音播报探索_iOS_06


1.3 升级JPush iOS SDK

v4.4.0: ​​ pod 'JPush' , '4.4.0'​


​​docs.jiguang.cn/changelog/j…​​

更新时间:2021-10-28

Change Log:

SDK适配ios15系统的本地通知功能

富媒体横屏异常兼容性处理


错误信息: ​​ld: library not found for -ljcore-ios-2.3.4​

原因:other linker flags 的信息没有自动更新

iOS15适配本地通知功能及语音播报探索_推送_07

解决方案:直接删除other linker flags的jcore信息即可

II 解决新的问题:iOS15 使用本地通知会显示横幅

为了避免iOS15 使用本地通知会显示横幅,采取新的播放方案:Notification Service Extension接到通知之后,去解析出下载播放的音频,下载完毕之后修改sound字段,交由系统播报。

2.1 更换之前的实现思路

iOS15之后的系统通过Notification Service Extension修改推送sounds字段来播报自定义的语音。

+ (instancetype)soundNamed:(UNNotificationSoundName)name;


This method searches for sound files in the following locations, in order:


The <app_container>/Library/Sounds directory, where <app_container> is the app’s container directory.


The <group_container>/Library/Sounds directory, where <group_container> is one of the app’s shared group container directories. For information about how to configure group containers for your app, see Configure app groups.


The main bundle of the current executable.

sounds除了播放工程主目录(main bundle,打包时候就内置在项目中)和Library/Sounds,还可以播放AppGroup中Library/Sounds的音频 ,于是乎,我们可以在后台合成,然后下载到AppGroup后修改sound字段进行播放。

当音频下载处理完成后调用 ​​self.contentHandler(self.bestAttemptContent);​​ 弹出顶部横幅,并开始播报,横幅消失时音频会停止,音频需要控制在在6s之内;下载失败播放默认语音。

如果多条推送同时到达,调用self.contentHandler(self.bestAttemptContent);后,可主动去阻塞线程一定的时长(音频时长)。

2.2 注意事项

  1. 音频支持的格式: aiff、caf、wav、MP3
  2. 如果服务端不配合,不提供合成语音片段的下载地址,就需要在本地合成语音。

2.3 核心代码实现

@implementation NotificationService

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];

self.bestAttemptContent.sound = [UNNotificationSound soundNamed:@"sound.wav"];

self.contentHandler(self.bestAttemptContent);


}