1

前言

随着人们对手机的依赖性越来越高,对于从手机获取信息也有了更多的要求。推送就是一项不可忽视的方案,它可以在用户没有打开APP下的情况下将信息及时的推送给用户。推送功能在运营上也有极为重要的意义,关乎到 用户体验留存 等问题。应当给予相当的重视。

iOS 系统上的推送经历了多年的发展,已经从最初那简单的信息展示 发展到现在可以支持更加丰富内容展示,图片,视频,音乐等等。用户可以在通知面板上查看详情,直接进行交互,而不用打开APP。

开发者可以为自己的 APP 进行界面的量身定制( NotificationContentExtension ),也可以在收到通知时截获通知内容,进行修改( NotificationServiceExtension )。

这篇文章是对 UNNotificationContentExtension(下文中简称 NotificationContent) 与 UNNotificationServiceExtension(下文中简称 NotificationService)的一个简要总结。

关键代码以及完整的Demo将在文末给出。

效果展示

功能简介: 一个展示图片的推送,实现点赞,评论功能,通过截取通知更改指定内容。


NotificationContentExtension

NotificationContent 可供开发者定制化通知界面的界面,开发者只需要在targets中添加 NotificationContent 的 Target即可 (创建NotificationService 时选择NotificationServiceExtension):

图 1.png

创建好选中的Extension后,我们看到项目生成了一个由我们命名的Target以及项目中一个文件夹(此处命名为NotificationContent),我们来详细看看文件夹里面都包含哪些内容:

图 2.png

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 是资源下载完成后缓存到 本地 的地址

对于媒体文件,支持的媒体类型以及资源大小限制如下图:

图 4.png

总的来说,附件方面要尽可能的在质量达标的前提下压缩其大小,避免过多的超时,异常,以达到最好的体验。


关键代码示例

在示例中推送的数据结构如下:

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地址

最后再说点什么
  1. 因为该功能需要 iOS 10 以上的系统支持,在编写代码时注意系统版本的区分。苹果再三强调推送面板上的交互要尽可能的简洁易用。
  2. 在示例的编写过程中,笔者在同时实现系统的文本输入框,以及自定义面板时,会造成输入框的 action 失效,可能是两者在响应上有冲突,等后面有空的时候再验证一下。
  3. 笔者在创建自定义视图时习惯性的直接使用了 xib ,结果编译器报错,不能导入。后来在自动生成的Storybord中创建,可以直接调用。

小小大熊猫
4 声望0 粉丝