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系统以上在后台或者被杀死无法语音播报的问题

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

若主工程 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

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

注意:

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服务已经创建成功之后,你的项目中包含两个方法。

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

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字段来播报自定义的语音。

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

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

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

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

准备资源文件Resource

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

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

上一篇:基于 Nginx 的 HTTPS 性能优化实践


下一篇:《iOS逆向》小程序的基础配置:文章的特色图片(缩略图/封面)模糊的解决方案