前言
随着人们对手机的依赖性越来越高,对于从手机获取信息也有了更多的要求。推送就是一项不可忽视的方案,它可以在用户没有打开APP下的情况下将信息及时的推送给用户。推送功能在运营上也有极为重要的意义,关乎到 用户体验 ,留存 等问题。应当给予相当的重视。
iOS 系统上的推送经历了多年的发展,已经从最初那简单的信息展示 发展到现在可以支持更加丰富内容展示,图片,视频,音乐等等。用户可以在通知面板上查看详情,直接进行交互,而不用打开APP。
开发者可以为自己的 APP 进行界面的量身定制( NotificationContentExtension ),也可以在收到通知时截获通知内容,进行修改( NotificationServiceExtension )。
这篇文章是对 UNNotificationContentExtension(下文中简称 NotificationContent) 与 UNNotificationServiceExtension(下文中简称 NotificationService)的一个简要总结。
关键代码以及完整的Demo将在文末给出。
效果展示
功能简介: 一个展示图片的推送,实现点赞,评论功能,通过截取通知更改指定内容。
NotificationContentExtension
NotificationContent 可供开发者定制化通知界面的界面,开发者只需要在targets中添加 NotificationContent 的 Target即可 (创建NotificationService 时选择NotificationServiceExtension):
创建好选中的Extension后,我们看到项目生成了一个由我们命名的Target以及项目中一个文件夹(此处命名为NotificationContent),我们来详细看看文件夹里面都包含哪些内容:
NotificationViewController
可以看到其继承自UIViewController。ViewController 中能干的事情它都能干。还配套了一个Storyboard 构建界面更加的方便。
在 .m 文件中可以看到其实现了UNNotificationContentExtension协议,
进入该协议可以看到只有两个方法:
//当接收到推送需要展示时,会调用这个方法,每一条推送都会调用这个方法。
- (void)didReceiveNotification:(UNNotification *)notification;
//用于获取用户的交互事件(Notification Actions)
- (void)didReceiveNotificationResponse:(UNNotificationResponse *)response completionHandler:(void (^)(UNNotificationContentExtensionResponseOption option))completion;
这里需要特别注意, 方法 didReceiveNotificationResponse 是用于获取Notification Actions的事件。
注意 :NotificationContent 面板上不支持“自定义”交互。
官方只提供了一个多媒体按钮可以添加在界面上, 协议中关于该按钮的部分如下:
//媒体按钮的类型
@property (nonatomic, readonly, assign) UNNotificationContentExtensionMediaPlayPauseButtonType mediaPlayPauseButtonType;
//媒体按钮的位置、大小
@property (nonatomic, readonly, assign) CGRect mediaPlayPauseButtonFrame;
//媒体按钮的颜色
@property (nonatomic, readonly, copy) UIColor *mediaPlayPauseButtonTintColor;
//媒体按钮事件
- (void)mediaPlay;
- (void)mediaPause;
开发者只能够对位置,颜色,以及其交互事件进行简单的控制。相信苹果是为了让推送的信息面板风格整体不会太杂乱。对于视频资源,控制按钮是不得不提供的(想想一下将视频控制的交互如果放在Notification Action上的画面,其效果,交互将非常别扭)
info.plist
这里面是 NotificationContent 的一些配置信息,其中需要注意几个关键的Key:
- UNNotificationExtensionCategory :用于标识当前 NotificationContent, 在接收到推送时,通过推送的 Category 参数来调用指定的 NotificationContent。默认是一个 String 类型,其也可以更改为 Array 类型,这表示一个 NotificationContent 可以代表多个 Category。
- UNNotificationExtensionDefaultContentHidden:是否隐藏默认的控件。自定义通知视图下,默认的控件(title, subtitle, body)都在控件最下方展示,可通过将此 key 改为YES 来进行隐藏。
- UNNotificationExtensionInitialContentSizeRatio: 视图初始化的高宽比,用于优化展示效果,具体数值依情况设置。
在调用我们的 NotificationContent 时,需要通过 配置 Category 来指定所调用的视图(与 info.plist 中UNNotificationExtensionCategory 相匹配)。配置部分如下:
NotificationServiceExtension
可以在通知展现给用户之前修改通知的内容。但是,静默推送,只播放声音或只修改推送条数的不能修改。
其文件结构如下:
我们只需要注意 NotificationService 即可,其继承自 UNNotificationServiceExtension ,在 .m 实现文件中,它重写了父类的两个方法:
// 通过调用 contentHandler 来传递修改后的推送内容
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent *contentToDeliver))contentHandler;
// 当在修改推送内容超时时会调用此方法
- (void)serviceExtensionTimeWillExpire;
我们通过 didReceiveNotificationRequest 方法拦截推送内容进行修改,附件的下载也在这里进行。处理的时间有限制,所以在下载资源会涉及到处理超时的问题,这时候就会会触发 serviceExtensionTimeWillExpire 方法,对修改通知进行最后的补救。不管怎样,将推送展现给用户是必须的。
想要触发推送拦截需要注意两个点:
[1] apns推送的字段中,必须包含 mutalbe-content ,值为 1。
[2] 推送必须是一个展示的视图(静默推送,只播放声音、修改推送数值的不会触发)。
关于 UNNotificationAttachment
在 UNMutableNotificationContent 中,我们可以看到一个名为 attachments 的集合。其要求集合中的元素都为 UNNotificationAttachment类型。
UNNotificationAttachment 是一个媒体文件的通知,在苹果的实现流程上,是它在 ServiceExtension 中将附件信息整理打包给ContentExtension。使用 attachmentWithIdentifier:URL:options:error: 进行创建。
Identifier 是资源的标识符
URL 是资源下载完成后缓存到 本地 的地址
对于媒体文件,支持的媒体类型以及资源大小限制如下图:
总的来说,附件方面要尽可能的在质量达标的前提下压缩其大小,避免过多的超时,异常,以达到最好的体验。
关键代码示例
在示例中推送的数据结构如下:
aps = {
alert = {
body = "XXX";
title = "XXX";
};
badge = 1;
category = "myNotificationCategory";
"mutable-content" = 1;
sound = default;
};
"image-url" = "XXX"; //图片链接
"last-comments" = "XXX"; //最近一条评论
iOS 10 中配置推送的代码
UNNotificationAction * likeAction; //喜欢
UNNotificationAction * ingnoreAction; //取消
UNTextInputNotificationAction * inputAction; //文本输入
likeAction = [UNNotificationAction actionWithIdentifier:@"action_like"
title:@"点赞"
options:UNNotificationActionOptionForeground];
inputAction = [UNTextInputNotificationAction actionWithIdentifier:@"action_input"
title:@"评论"
options:UNNotificationActionOptionForeground
textInputButtonTitle:@"发送"
textInputPlaceholder:@"说点什么"];
ingnoreAction = [UNNotificationAction actionWithIdentifier:@"action_cancel"
title:@"忽略"
options:UNNotificationActionOptionForeground];
//下面的Identifier 需与 NotificationContent的info.plist 文件中所配置的 UNNotificationExtensionCategory 一致,
//本示例中为“myNotificationCategory”
UNNotificationCategory * category;
category = [UNNotificationCategory categoryWithIdentifier:@"myNotificationCategory"
actions:@[likeAction, inputAction, ingnoreAction]
intentIdentifiers:@[]
options:UNNotificationCategoryOptionNone];
NSSet * sets = [NSSet setWithObjects:category, nil];
[[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:sets];
NotificationService.m
当接收到通知,展示给用户前可在此对推送来的数据进行拦截、修改。注意超时、处理异常等问题。
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
// 修改信息
NSString * type = @"【特别关心】 ";
self.bestAttemptContent.title = [type stringByAppendingString:self.bestAttemptContent.title];
// 下载并关联附件
NSString * urlString = self.bestAttemptContent.userInfo[@"image-url"];
[self loadAttachmentForUrlString:urlString
completionHandler: ^(UNNotificationAttachment *attachment) {
self.bestAttemptContent.attachments = [NSArray arrayWithObjects:attachment, nil];
[self contentComplete];
}];
}
这里也将附件的处理方法贴出来供大家参考:
- (void)loadAttachmentForUrlString:(NSString *)urlString
completionHandler:(void (^)(UNNotificationAttachment *))completionHandler {
__block UNNotificationAttachment *attachment = nil;
__block NSURL *attachmentURL = [NSURL URLWithString:urlString];
NSString *fileExt = [@"." stringByAppendingString:[urlString pathExtension]];
//下载附件
_session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSURLSessionDownloadTask *task;
task = [_session downloadTaskWithURL:attachmentURL
completionHandler: ^(NSURL *temporaryFileLocation, NSURLResponse *response, NSError *error) {
if (error != nil) {
NSLog(@"%@", error.localizedDescription);
} else {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *localURL = [NSURL fileURLWithPath:[temporaryFileLocation.path
stringByAppendingString:fileExt]];
[fileManager moveItemAtURL:temporaryFileLocation
toURL:localURL
error:&error];
NSError *attachmentError = nil;
NSString * uuidString = [[NSUUID UUID] UUIDString];
//将附件信息进行打包
attachment = [UNNotificationAttachment attachmentWithIdentifier:uuidString
URL:localURL
options:nil
error:&attachmentError];
if (attachmentError) {
NSLog(@"%@", attachmentError.localizedDescription);
}
}
completionHandler(attachment);
}];
[task resume];
}
NotificationContent
这里重点说一下如何提取传递过来的附件信息。
- (void)didReceiveNotification:(UNNotification *)notification {
/*
* 这里有一堆普通数据展示逻辑
*/
//附件的提取,
//这里必须注意,startAccessingXXX方法 与 stopAccessingXXX 方法是成对出现的
UNNotificationAttachment * attachment = notification.request.content.attachments[0];
if ([attachment.URL startAccessingSecurityScopedResource]) {
NSData *imageData = [NSData dataWithContentsOfURL:attachment.URL];
[self.imageView setImage:[UIImage imageWithData:imageData]];
[attachment.URL stopAccessingSecurityScopedResource];
}
}
didReceiveNotificationResponse 中可以处理Notification Actions的事件,这也就让推送视图上的交互效果得以成为现实,例如示例中的“评论”, “点赞”功能。
- (void)didReceiveNotificationResponse:(UNNotificationResponse *)response completionHandler:(void (^)(UNNotificationContentExtensionResponseOption option))completion {
if ([response.actionIdentifier isEqualToString:@"action_like"]) {
//点赞
[self.likeLabel setHidden:!self.likeLabel.hidden];
}else if([response.actionIdentifier isEqualToString:@"action_input"]) {
//发送评价
UNTextInputNotificationResponse * textResponse = (UNTextInputNotificationResponse *)response;
[self postComment:textResponse.userText];
} else {
//忽略
completion(UNNotificationContentExtensionResponseOptionDismiss);
}
completion(UNNotificationContentExtensionResponseOptionDoNotDismiss);
}
关于自定义交互
在推送的交互上,苹果也提供了自定义交互面板,完全由开发者自定义, 例如个性化的表情面板等,只需要开发者在 NotificationContent 中重写两个方法:
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (UIView *)inputView {
return _customView; //自定义交互视图
}
而在需要唤起自定义交互视图时,调用自定义视图的becomeFirstResponder即可,例如这里举例的customView:
[self.customView becomeFirstResponder];
笔者在测试自定义交互时,所指定的视图并不受设置的frame影响。始终从屏幕下方弹出,位置,大小始终如一,这个有待进一步测试。
详细的代码可以参看Demo
Demo地址
最后再说点什么
- 因为该功能需要 iOS 10 以上的系统支持,在编写代码时注意系统版本的区分。苹果再三强调推送面板上的交互要尽可能的简洁易用。
- 在示例的编写过程中,笔者在同时实现系统的文本输入框,以及自定义面板时,会造成输入框的 action 失效,可能是两者在响应上有冲突,等后面有空的时候再验证一下。
- 笔者在创建自定义视图时习惯性的直接使用了 xib ,结果编译器报错,不能导入。后来在自动生成的Storybord中创建,可以直接调用。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。