iOS小技能:消息推送扩展的使用

简介: iOS小技能:消息推送扩展的使用

引言

iOS15引入了消息推送的新属性中断级别interruptionLevel,具体的枚举值

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));
  • Passive:被动类型的通知不会使手机亮屏并且不会播放声音。
  • Active: 活动类型的通知会使手机亮屏且会播放声音,为默认类型。
  • Time Sensitive(时效性):会使手机亮屏且会播放声音;可能会在免打扰模式(焦点模式)下展示。
  • Critical(关键):会立刻展示,亮屏,播放声音,无效免打扰模式,并且能够绕过静音,如果没有设置声音则会使用一种默认的声音。

因此当我们的消息推送比较重要的时候,比如收款到账的通知,可以利用消息推送扩展来修改消息推送的中断级别为时效性,这样手机接收的时候会亮屏且会播放声音;即使在免打扰模式(焦点模式)下也会展示。

我们也可以通过Notification Service Extension修改推送sounds字段来播报自定义的语音。

I Service Extension开发步骤

实现方式:采用Service Extension并结合本地通知进行实现。

iOS 10新增了Service Extension,这意味着在APNs到达我们的设备之前,还会经过一层允许用户自主设置的Extension服务进行处理,为APNs增加了多样性。

本文就是利用Service Extension处理消息并语言播报,来解决iOS12.1系统以上在后台或者被杀死无法语音播报的问题

image.png

若主工程 Target 最低支持版本小于10.0,扩展 Target 系统版本设置为10.0。

若主工程 Target 最低支持版本大于10.0,则扩展 Target 系统版本与主工程 Target 版本一致。

通知的内容中 mutable-content 字段必须为1

demo下载:https://download.csdn.net/download/u011018979/14026303

1.1 创建NotificationServiceExtension

  • 新建Notification Service Extension

image.png

注意:

1、Service Extension的Bundle Identifier不能和Main Target(也就是你自己的App Target)的Bundle Identifier相同,否则会报BundeID重复的错误。

2、Service Extension的Bundle Identifier需要在Main Target的命名空间下,比如说Main Target的BundleID为io.re.xxx,那么Service Extension的BundleID应该类似与io.re.xxx.yyy这样的格式。

  • 创建NotificationService.m继承UNNotificationServiceExtension ,并实现方法- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler

Service Extension服务已经创建成功之后,你的项目中包含两个方法。

image.png

1、didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent *contentToDeliver))contentHandlerAPNs到来的时候会调用这个方法,此时你可以对推送过来的内容进行处理,然后使用contentHandler完成这次处理。但是如果时间太长了,APNs就会原样显示出来。也就是说,我们可以在这个方法中处理我们的通知,个性化展示给用户。

2、serviceExtensionTimeWillExpire而serviceExtensionTimeWillExpire方法,会在过期之前进行回调,此时你可以对你的APNs消息进行一下紧急处理。

  • NotificationService.m
@interface NotificationService ()
@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@end
@implementation NotificationService
/**
Call contentHandler with the modified notification content to deliver. If the handler is not called before the service's time expires then the unmodified notification will be delivered
APNs到来的时候会调用这个方法,此时你可以对推送过来的内容进行处理,然后使用contentHandler完成这次处理。但是如果时间太长了,APNs就会原样显示出来。 也就是说,我们可以在这个方法中处理我们的通知,个性化展示给用户。 
*/
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    self.contentHandler = contentHandler;
    self.bestAttemptContent = [request.content mutableCopy];
    NSLog(@"NotificationService_%@: dict->%@", NSStringFromClass([self class]), self.bestAttemptContent.userInfo);
    self.bestAttemptContent.sound = nil;
#warning 如果系统大于12.0就走语音包合成文件播报方法
    if (yjIOS10) {
        __weak typeof(self) weakSelf = self;
        [[KNAudioTool sharedPlayer] playPushInfo:weakSelf.bestAttemptContent.userInfo backModes:YES completed:^(BOOL success) {
            __strong typeof(weakSelf) strongSelf = weakSelf;
            if (strongSelf) {
                NSMutableDictionary *dict = [strongSelf.bestAttemptContent.userInfo mutableCopy] ;
                    [dict setObject:[NSNumber numberWithBool:YES] forKey:@"hasHandled"] ;
                strongSelf.bestAttemptContent.userInfo = dict;
                strongSelf.contentHandler(self.bestAttemptContent);
            }
        }];
    } else {
        self.contentHandler(self.bestAttemptContent);
    }
}
/**
而serviceExtensionTimeWillExpire方法,会在过期之前进行回调,此时你可以对你的APNs消息进行一下紧急处理。
*/
- (void)serviceExtensionTimeWillExpire {
    // Called just before the extension will be terminated by the system.
    // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
    self.contentHandler(self.bestAttemptContent);
}

1.2 创建 AudioTool

iOS12.1 以下使用AVAudioPlayer进行语音播报。iOS12.1 - iOS14 可以使用本地通知进行语音播报。iOS15 通过修改推送sounds字段来播报自定义的语音。

image.png

后台或者锁屏状态下播放音频文件

AVAudio Session的Category值需要使用AVAudioSessionCategoryPlaybackAVAudioSessionCategoryPlayAndRecord

[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:NULL];
            [[AVAudioSession sharedInstance] setActive:YES error:NULL];
            [self playAudioFiles];

CategoryOptions根据实际需要可选择

MixWithOthers(与其他声音混音) DuckOthers(调低其他声音的音量)

1.3 配置项目

  • 集成JPush
pod 'JPush'

如果遇到这个问题:

CDN: trunk URL couldn't be downloaded: https://raw.githubusercontent.com/CocoaPods/Specs/master/Specs/b/0/d/JPush/3.3.3/JPush.podspec.json Response: Couldn't connect to server

添加一下官方source即可

source 'https://github.com/CocoaPods/Specs.git'
  • 添加 push notification 及background modes

image.png

准备资源文件Resource

image.png

1.4、注册推送

registerJPUSH

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    if (yjIOS10) {
        //通知授权
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        center.delegate = self;
        [center requestAuthorizationWithOptions:UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionSound completionHandler:^(BOOL granted, NSError * _Nullable error) {
            if (granted) {
                // 点击允许
                [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
                    NSLog(@"yangjing_%@: settings->%@", NSStringFromClass([self class]),settings);
                }];
            } else {
                // 点击不允许
            }
        }];
        [[UIApplication sharedApplication] registerForRemoteNotifications];
    } else {
        // iOS8-iOS10注册远程通知的方法
        UIUserNotificationType types = UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound;
        UIUserNotificationSettings *mySettings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
        [[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];
        [[UIApplication sharedApplication] registerForRemoteNotifications];
    }
    //初始化JPushSDK
    [[JPushTool shareTool] registerJPUSH:launchOptions];
    return YES;
}
  • 通知事件处理
- (void)applicationDidEnterBackground:(UIApplication *)application {
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    [UIApplication sharedApplication].applicationIconBadgeNumber = 0;
    [[UIApplication sharedApplication] cancelAllLocalNotifications];
    [[JPushTool shareTool] setBadge:0];
}
- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(nonnull UIUserNotificationSettings *)notificationSettings {
    // register to receive notifications
    [application registerForRemoteNotifications];
}
//远程推送注册成功
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    NSLog(@"zkn%@: deviceToken->%@", NSStringFromClass([self class]), [deviceToken description]);
    [[JPushTool shareTool] registerForRemoteNotificationsWithDeviceToken:deviceToken];
}
//远程推送注册失败
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
}
//ios10之前接收远程推送
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
    NSLog(@"yangjing_%@: userInfo->%@ ", NSStringFromClass([self class]), userInfo);
    [[KNAudioTool sharedPlayer] playPushInfo:userInfo backModes:NO completed:nil];
}
//ios10之前接收本地推送
- (void)application:(UIApplication *)app didReceiveLocalNotification:(UILocalNotification *)notif {
}
//ios10之后接收推送
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler  API_AVAILABLE(ios(10.0)){
    NSDictionary * userInfo = notification.request.content.userInfo;
    //远程推送
    if([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
        NSLog(@"%@: userInfo->%@ ", NSStringFromClass([self class]), userInfo);
        //未经过NotificationService处理
        if (![userInfo.allKeys containsObject:@"hasHandled"]) {
            if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {
                [[KNAudioTool sharedPlayer] playPushInfo:userInfo backModes:NO completed:nil];
                completionHandler(UNNotificationPresentationOptionAlert);
            } else {
                completionHandler(UNNotificationPresentationOptionBadge|UNNotificationPresentationOptionAlert|UNNotificationPresentationOptionSound);
            }
        } else {
            if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {
                completionHandler(UNNotificationPresentationOptionAlert);
            } else {
                completionHandler(UNNotificationPresentationOptionBadge|UNNotificationPresentationOptionAlert);
            }
        }
    }
    //远程推送
    else {
        completionHandler(UNNotificationPresentationOptionBadge|UNNotificationPresentationOptionAlert|UNAuthorizationOptionSound);
    }
}
// iOS10及以上通知的点击事件
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler  API_AVAILABLE(ios(10.0)) {
    completionHandler();  // 系统要求执行这个方法
}

see also

目录
相关文章
|
JSON JavaScript 前端开发
iOS小技能: 开发 uni-app 原生插件(支持iOS Extension)
术语:uni原生插件指的是将`原生开发的功能按照规范封装成插件包`,然后即可在 uni-app 前端项目中通过js调用原生能力。
1053 0
iOS小技能: 开发 uni-app 原生插件(支持iOS Extension)
|
文字识别 API iOS开发
iOS小技能:iOS13 证件扫描 & 文字识别API
1. 应用场景:证件扫描、文字识别 2. 原理:利用iOS13 VNDocumentCameraViewController的证件扫描和VNRecognizeTextRequest文字识别功能进行实现
370 0
iOS小技能:iOS13 证件扫描 & 文字识别API
|
iOS开发
iOS开发-聊天气泡的绘制和聊天消息列表
iOS开发-聊天气泡的绘制和聊天消息列表
227 0
iOS开发-聊天气泡的绘制和聊天消息列表
|
安全 iOS开发
iOS小技能:下拉刷新控件的适配
1. 下拉顶部背景色设置: 往tableView的父控件添加拉伸背景视图 2. present 半屏适配 iOS13 modalPresentationStyle属性默认不是全屏样式`UIModalPresentationFullScreen`,而是半屏样式,需要根据需求手动设置。 present 半屏,会导致列表下拉刷新失效。
198 0
iOS小技能:下拉刷新控件的适配
|
iOS开发 Python
iOS小技能:lldb打印block参数签名
iOS逆向时经常会遇到参数为block类型,本文介绍一个lldb script,可快速打印出Objective-C方法中block参数的类型。
186 0
iOS小技能:lldb打印block参数签名
|
JSON JavaScript 前端开发
iOS小技能: 开发 uni 原生插件(支持iOS Extension)
背景:app采用uni实现 需求: iOS App前台后台离线(杀死情况下)推送语音播报(到账xx元、收款播报、自定义推送铃)。 实现方式:uni-app 原生插件(支持iOS Extension)
444 0
iOS小技能: 开发 uni 原生插件(支持iOS Extension)
|
安全 iOS开发 开发者
iOS小技能:重签名、打包脚本
重签名需求:改变了应用的二进制文件,或者增加、修改了应用里面的资源,应用本身的签名就会被破坏。
260 0
iOS小技能:重签名、打包脚本
|
IDE Unix 编译器
iOS小技能:Makefile的使用(Makefile的规则、部署脚本、config管理ssh连接)
make是一个命令工具,是一个解释makefile中指令的命令工具。其本质是**文件依赖**,Makefile文件制定编译和链接所涉及的文件、框架、库等信息,将整个过程自动化。
375 0
|
编解码 自然语言处理 API
iOS小技能:通讯录
iOS处理语言工具CFStringTransform : 智能地处理用户的输入内容,经典应用场景【索引】
101 0