iOS UNNotificationServiceExtension(通知服务扩展)和 UNNotificationContentExtension

2,577 阅读19分钟

iOS10之后的通知具有扩展功能,可以在系统收到通知、展示通知时做一些事情。下面是实现步骤要点介绍:

1. UNNotificationServiceExtension

  • UNNotificationServiceExtension:通知服务扩展,是在收到通知后,在通知显示给用户之前会给你时间处理通知payloads,此时可以对通知的 request.content 进行内容添加,如添加附件,userInfo 等。例如将要展示在通知中的image、GIF、Video、解密文本下载下来。点击查看官网文档

  • iOS10给通知添加附件有两种情况:本地通知和远程通知。

    • 本地推送通知,只需给content.attachments设置UNNotificationAttachment附件对象
    • 远程推送通知,需要实现 UNNotificationServiceExtension(通知服务扩展),在回调方法中处理 推送内容时设置 request.content.attachments(请求内容的附件) 属性,之后调用 contentHandler 方法即可。

1.1 通知payload字段

如果要支持扩展服务,能对通知的 request.content 进行内容添加,必须给apns增加 "mutable-content":1 字段,使你的推送通知是动态可变的。

服务器向设备发送通知, 这个通知消息的格式是有要求的,有以下几点需要注意:

  • aps字段必须要有,不然收不到通知
  • aps字段下alert字段必须要有,不然也收不到通知
  • alert字段的值是字符串的时候,不可为空,不然的话你虽然收的到通知,但是是手机除了震动或者声音,没有任何提示
  • alert字段的值是字典的时候,下面这三个字段必须要有一个,不然和上面一样。就是一句话alert的值必须是可以使用的
  • mutable-content字段值最好是1,目前没见过其他值
  • 如果你想在iOS10上展示位富文本的推送格式,category字段必须带,值必须在info.plist文件中能找的到的
{
"aps":
    {
        "alert":
        {
            "title":"iOS10远程推送标题",
            "subtitle" : "iOS10 远程推送副标题",
            "body":"这是在iOS10以上版本的推送内容,并且携带来一个图片附件"
        },
        "badge":1,
        "sound":"default",
        "mutable-content":1,
        "media":"image",
        "image-url":"https://tva1.sinaimg.cn/large/008i3skNgy1gtmd6b4whhj60fq0g6tb502.jpg"
    }
}

1.2 创建Service Extension

使用Xcode打开项目,选中File -> New -> Target...,在出现的弹窗中选择 Notification Service Extension 模板。如下图所示:

image.png

点击Next后,你需要填写特定于应用程序的相关信息。添加完毕,点击Finish可以在项目的TARGETS里看到多了Service Extension一项。如图所示:

image.png

而项目中则会生成 NotificationService 文件夹,以及相应的类文件和plist文件,如图所示:

image.png

1.3 Extension LifeCycle

一旦你给App配置了通知服务扩展程序后,每个通知都会执行以下过程:

  • App收到通知。
  • 系统创建扩展类的实例对象并在后台启动它。
  • 你的扩展程序会执行内容编辑和/或下载某些内容操作。
  • 如果你的扩展程序执行太长时间而不能完成它的工作,将会收到通知并被立即终止。
  • 通知显示给用户。

1.4 Extension Code

NotificationService 类文件中有两个回调方法,方法如下:

// 重写此方法以实现推送通知的修改
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler;
// 扩展程序被系统终止之前会被调用,你可以选择是否覆盖此方法
- (void)serviceExtensionTimeWillExpire;

系统收到通知后,在有限的时间(不超过30s)内,你的扩展程序可以在didReceiveNotificationRequest:withContentHandler:方法内对通知做相应的更改并执行contentHandler代码块。如果你没有及时执行,系统将会调用上面的第二个方法serviceExtensionTimeWillExpire,在这里给你提供最后一次执行contentHandler代码块的机会。如果你什么都没做,系统将向用户显示通知的原始内容,你做的所有修改都不会生效。

示例:收到通知后,给通知增加图片附件

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    self.contentHandler = contentHandler;
    self.bestAttemptContent = [request.content mutableCopy];
    self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title];
 
    //1. 下载
    NSURL *url = [NSURL URLWithString:@"http://img1.gtimg.com/sports/pics/hv1/194/44/2136/138904814.jpg"];
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
    NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (!error) {
 
            //2. 保存数据
            NSString *path = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject
                              stringByAppendingPathComponent:@"download/image.jpg"];
            UIImage *image = [UIImage imageWithData:data];
            NSError *err = nil;
            [UIImageJPEGRepresentation(image, 1) writeToFile:path options:NSAtomicWrite error:&err];
 
            //3. 添加附件
            UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:@"remote-atta1" URL:[NSURL fileURLWithPath:path] options:nil error:&err];
            if (attachment) {
                self.bestAttemptContent.attachments = @[attachment];
            }
        }
 
        //4. 返回新的通知内容
        self.contentHandler(self.bestAttemptContent);
    }];
 
    [task resume];
}

使用UNNotificationServiceExtension,你有30秒的时间处理这个通知,可以同步下载图像和视频到本地,然后包装为一个UNNotificationAttachment扔给通知,这样就能展示用服务器获取的图像或者视频了。这里需要注意:如果数据处理失败,超时,extension会报一个崩溃信息,但是通知会用默认的形式展示出来,app不会崩溃。

1.5 本地推送通知

附件通知所带的附件格式大小都是有限的。

  • UNNotificationRequest

创建一个UNNotificationRequest类的实例,一定要为它设置identifier, 在后面的查找,更新, 删除通知,这个标识是可以用来区分这个通知与其他通知。

把request加到UNUserNotificationCenter, 并设置触发器,等待触发, 如果另一个request具有和之前request相同的标识,不同的内容, 可以达到更新通知的目的

创建一个本地通知我们应该先创建一个UNNotificationRequest类,并且将这个类添加到UNUserNotificationCenter才可以。代码如下:

    // 1.创建通知内容
    UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
    content.title = @"徐不同测试通知";
    content.subtitle = @"测试通知";
    content.body = @"来自徐不同的简书";
    content.badge = @1;
    NSError *error = nil;
    NSString *path = [[NSBundle mainBundle] pathForResource:@"icon_certification_status1@2x" ofType:@"png"];
    // 2.设置通知附件内容
    UNNotificationAttachment *att = [UNNotificationAttachment attachmentWithIdentifier:@"att1" URL:[NSURL fileURLWithPath:path] options:nil error:&error];
    if (error) {
        NSLog(@"attachment error %@", error);
    }
    content.attachments = @[att];
    content.launchImageName = @"icon_certification_status1@2x";
    // 2.设置声音
    UNNotificationSound *sound = [UNNotificationSound defaultSound];
    content.sound = sound;

    // 3.触发模式
    UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:5 repeats:NO];

    // 4.设置UNNotificationRequest
    NSString *requestIdentifer = @"TestRequest";
    UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:requestIdentifer content:content trigger:trigger1];
    //5.把通知加到UNUserNotificationCenter, 到指定触发点会被触发
    [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
    }];

通过以上代码,我们就可以创建一个5秒触发本地通知,具体样式可以看下图

image.png

  • UNNotificationContentUNMutableNotificationContent(通知内容和可变通知内容)

通知内容分为可变的以及不可变的两种类型,类似于可变数组跟不可变数组。后续我们通过某一特定标识符更新通知,便是用可变通知了。
不管是可变通知还是不可变通知,都有以下的几个属性:

// 1.附件数组,存放UNNotificationAttachment类
@property (NS_NONATOMIC_IOSONLY, copy) NSArray <UNNotificationAttachment *> *attachments ;

// 2.应用程序角标,0或者不传,意味着角标消失
@property (NS_NONATOMIC_IOSONLY, copy, nullable) NSNumber *badge;

// 3.主体内容
@property (NS_NONATOMIC_IOSONLY, copy) NSString *body ;

// 4.app通知下拉预览时候展示的图
@property (NS_NONATOMIC_IOSONLY, copy) NSString *launchImageName;

// 5.UNNotificationSound类,可以设置默认声音,或者指定名称的声音
@property (NS_NONATOMIC_IOSONLY, copy, nullable) UNNotificationSound *sound ;

// 6.推送内容的子标题
@property (NS_NONATOMIC_IOSONLY, copy) NSString *subtitle ;

// 7.通知线程的标识
@property (NS_NONATOMIC_IOSONLY, copy) NSString *threadIdentifier;

// 8.推送内容的标题
@property (NS_NONATOMIC_IOSONLY, copy) NSString *title ;

// 9.远程通知推送内容
@property (NS_NONATOMIC_IOSONLY, copy) NSDictionary *userInfo;

// 10.category标识
@property (NS_NONATOMIC_IOSONLY, copy) NSString *categoryIdentifier;
  • UNNotificationAttachment (附件内容通知)

UNNotificationContent 类中,有个附件数组的属性,这就是包含 UNNotificationAttachment 类的数组了。

@property (NS_NONATOMIC_IOSONLY, copy) NSArray <UNNotificationAttachment *> *attachments ;

苹果的解释说,UNNotificationAttachment(附件通知)是指可以包含音频,图像或视频内容,并且可以将其内容显示出来的通知。使用本地通知时,可以在通知创建时,将附件加入即可。对于远程通知,则必须实现使用 UNNotificationServiceExtension 类通知服务扩展。

创建附件的方法是 attachmentWithIdentifier:URL:options:error: ,在使用时,必须指定使用文件附件的内容,并且文件格式必须是支持的类型之一。创建附件后,将其分配给内容对象的附件属性。 (对于远程通知,您必须从您的服务扩展做到这一点。)

附件通知支持的类型如下图:

image.png

下面是创建UNNotificationAttachment的方法:

+ (nullable instancetype)attachmentWithIdentifier:(NSString *)identifier URL:(NSURL *)URL options:(nullable NSDictionary *)options error:(NSError *__nullable *__nullable)error; 

注意:URL必须是一个有效的文件路径,不然会报错

options的属性, 一共有4种选项:

(1)UNNotificationAttachmentOptionsTypeHintKey此键的值是一个包含描述文件的类型统一类型标识符(UTI)一个NSString。如果不提供该键,附件的文件扩展名来确定其类型,常用的类型标识符有

kUTTypeImage,kUTTypeJPEG2000,kUTTypeTIFF,kUTTypePICT,kUTTypeGIF ,kUTTypePNG,kUTTypeQuickTimeImage等。看到这里你一定有疑问,这些类型导入报错了啊!!我研究了苹果文档,发现大家需要添加以下框架才可以,具体大家可以通过以下类型来处理。
注意:
框架就是#import<MobileCoreServices/MobileCoreServices.h>

使用方法如下:

dict[UNNotificationAttachmentOptionsTypeHintKey] = (__bridge id _Nullable)(kUTTypeImage);

(2)UNNotificationAttachmentOptionsThumbnailHiddenKey是一个BOOL值,为YES时候,缩略图将隐藏,默认为YES。

image.png 使用方法如下:

    dict[UNNotificationAttachmentOptionsThumbnailHiddenKey] =  @YES;
(3)UNNotificationAttachmentOptionsThumbnailClippingRectKey 剪贴矩形的缩略图。这个密钥的值是包含一个归一化的 CGRect - 也就是说,一个单元的矩形,其值是在以1.0〜 0.0 ,表示要显示的原始图像的所述部分的字典。例如,指定的(0.25 , 0.25)的原点和大小(0.5 ,0.5 )定义了剪辑矩形,只显示图像的中心部分。使用 CGRectCreateDictionaryRepresentation 函数来创建字典的矩形。

image.png

整张图被分割了,整体比例为1,如果想得到图中阴影面积,就需要写的CGRect(0.5,0.5,0.25,0.25),意思是,从(0.5,0.5)为原点,面积为(0.25,0.25),大家可以理解成,即下面的方法

    dict[UNNotificationAttachmentOptionsThumbnailClippingRectKey] = (__bridge id _Nullable)((CGRectCreateDictionaryRepresentation(CGRectMake(0.5, 0.5, 0.25 ,0.25))));

使用上面的方法,可以得到一张图的阴影部分的图像,这张图像会是通知的缩略图。比如我下面的这个图,缩略图大家应该可以发现变了吧。

image.png

这里为了理解,在给大家说几个"坐标点": (0,0,0.25,0.25)左上角的最小正方形 (0,0,0.5,0.5) 四分之一的正方形,左上角 (0.5,0.5,0.5,0.5)四分之一的正方形,右下角 (0.5,0,0.5,0.5)四分之一的正方形,左下角 (0.25,0.25,0.5,0.5)最中心的正方形

(4)UNNotificationAttachmentOptionsThumbnailTimeKey 一般影片附件会用到,指的是用影片中的某一秒来做这个缩略图;

使用方法如下:

    dict[UNNotificationAttachmentOptionsThumbnailTimeKey] =@10;

这里我们可以直接传递一个NSNumber的数值,比如使用影片第10s的画面来做缩略图就按照上面的来写。此外,要注意的是,这个秒数必须是这个影片长度范围内的,不然报错。

  • UNTimeIntervalNotificationTrigger (通知触发模式)

    (1) UNPushNotificationTrigger (远程通知触发)一般我们不会使用的

    (2) UNTimeIntervalNotificationTrigger (本地通知)

    一定时间之后,重复或者不重复推送通知。我们可以设置timeInterval(时间间隔)和repeats(是否重复)。

    使用方法:

    UNTimeIntervalNotificationTrigger *triggerOne =[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:5 repeats:NO];
    

    解释:上面的方法是指5秒钟之后执行。repeats这个属性,如果需要为重复执行的,则TimeInterval必须大于60s,否则会报*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'time interval must be at least 60 if repeating'的错误! (2) UNCalendarNotificationTrigger(本地通知)
    一定日期之后,重复或者不重复推送通知 例如,你每天8点推送一个通知,只需要 dateComponents 为8。如果你想每天8点都推送这个通知,只要 repeatsYES 就可以了。

// 周一早上 8:00 上班
NSDateComponents *components = [[NSDateComponents alloc] init];
// 注意,weekday是从周日开始的,如果想设置为从周一开始,大家可以自己想想~
components.weekday = 2; components.hour = 8; UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:YES]; 
  • UNLocationNotificationTrigger (本地通知)地理位置的一种通知,使用这个通知,你需要导入 #import<CoreLocation/CoreLocation.h>这个系统类库。示例代码如下:
 
//1、如果用户进入或者走出某个区域会调用下面两个方法
- (void)locationManager:(CLLocationManager *)manager
    didEnterRegion:(CLRegion *)region
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region代理方法反馈相关信息 //2、一到某个经纬度就通知,判断包含某一点么 // 不建议使用!!!!!!CLRegion *region = [[CLRegion alloc] init];// 不建议使用!!!!!! CLCircularRegion *circlarRegin = [[CLCircularRegion alloc] init]; [circlarRegin containsCoordinate:(CLLocationCoordinate2D)]; UNLocationNotificationTrigger *trigger4 = [UNLocationNotificationTrigger triggerWithRegion:circlarRegin repeats:NO]; 

注意,这里建议使用CLCircularRegion这个继承自CLRegion的类。

2. UNNotificationContentExtension

  • UNNotificationContentExtension:通知内容扩展,是在展示通知时展示一个自定义的用户界面。点击查看官网文档

Notification Content Extension(通知内容扩展)允许开发者加入自定义的界面,在这个界面里面,你可以绘制任何你想要的东西。但是有一个最重要的限制就是,这个自定义的界面没有交互。它们不能接受点击事件,用户并不能点击它们。但是推送通知还是可以继续与用户进行交互,因为用户可以使用 notificaitonactions 。注意:extension 也可以处理这些 actions

1.1 推送界面的组成

image.png

  • header的UI是系统提供的一套标准的UI。这套UI会提供给所有的推送通知。
  • header下面的custom content是自定义的内容,就是Notification Content Extension(通知内容扩展)。在这里,就可以显示任何你想绘制的内容了。你可以展示任何额外的有用的信息给用户。
  • default content是系统的界面。这也就是iOS 9 之前的推送的样子。
  • 最下面的notification action,在这一段,用户可以触发一些操作。并且这些操作还会相应的反映到上面的自定义的推送界面content extension中。

1.2 创建 Content Extension

使用Xcode打开项目,选中File -> New -> Target...,在出现的弹窗中选择 Notification Content Extension 模板。如下图所示:

image.png

创建新的Notification Content的target后。Xcode自动生成一个新的模板,下面有三个文件,ViewController、main Interface storyboard、info.plist。

image.png

然后打开这里的 ViewController

#import "NotificationViewController.h"
#import <UserNotifications/UserNotifications.h>
#import <UserNotificationsUI/UserNotificationsUI.h>
 
@interface NotificationViewController () <UNNotificationContentExtension>
 
@property IBOutlet UILabel *label;
 
@end
 
@implementation NotificationViewController
 
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any required interface initialization here.
}
 
- (void)didReceiveNotification:(UNNotification *)notification {
    self.label.text = notification.request.content.body;
}
 
@end

发现这里的 ViewController 就是一个普通的 UIViewController , 但是它实现了UNNotificationContentExtension 协议。 UNNotificationContentExtension 协议有一个 required 方法 didReceiveNotification: 。当收到指定 categroy 的推送时, didReceiveNotification: 方法会随着 ViewController 的生命周期方法,一起被调用,这样就能接受 notification object,更新UI。

1.3 配置category

接下来就是要让推送到达后,系统怎样找到自定义的UI。这时候就需要配置 extensioninfo.plist 文件。

image.png

这里和我们给 notification actions 注册 category 一样,给这个通知扩展指定相应的 category。在 UNNotificationExtensionCategory 字段里写入相应的 category id。值得提到的一点是,这里对应的 category 是可以为一个数组的,里面可以为多个 category ,这样做的目的是多个 category 共用同一套UI。

image.png

上图中 category idmyNotificationCategory1myNotificationCategory2 的通知就共用了一套UI。

设置了 category 后, 只要在通知里面增加 category 字段,值是上面在 extensionplist 里面配置的 category id, 收到的通知就会通过自定义的样式显示。

远程通知在 apns 里面增加 category 字段。

{
    "aps":{
        "alert":"Testing.. (34)",
         "badge":1,
           "sound":"default",
        "category":"myNotificationCategory1"
     }
}

1.4 自定义UI

示例:

- (void)didReceiveNotification:(UNNotification *)notification {
    self.label.text = [NSString stringWithFormat:@"%@ [modified]", notification.request.content.title];
    self.subLabel.text = [NSString stringWithFormat:@"%@ [modified]", notification.request.content.body];
    self.imageView.image = [UIImage imageNamed:@"hong.png"];
}

可以在 ViewController 中增加一些 LabelImageView ,收到通知的时候,提取想要的内容,或者添加额外的内容,设置到我们自定义的 View 上。

image.png

1.5 UI界面优化

优化一:发现是自定义界面的大小很不美观

这时候可以通过设置 ViewControllerpreferredContentSize 大小,控制自定义视图的大小。 也可以通过约束,控制自定义视图的大小。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.preferredContentSize = CGSizeMake(CGRectGetWidth(self.view.frame), 100);
}

优化二:目标大小的问题解决了,但是发现视图恢复成正确的尺寸前,先展示有一大片空白的样子,然后变成正确的样子。当通知展示出来之后,它的大小并不是正常的我们想要的尺寸。iOS系统会去做一个动画来Resize它的大小。这样体验很差。

image.png

会出现上面这张图的原因是,在推送送达的那一刻,iOS系统需要知道我们推送界面的最终大小。但是我们自定义的extension 在系统打算展示推送通知的那一刻,并还没有启动。所以这个时候,在我们代码都还没有跑起来之前,我们需要告诉iOS系统,我们的 View 最终要展示的大小。

为了解决这个问题,我们需要在 extensioninfo.plist 里设置一个 content size ratio 。增加字段UNNotificationExtensionInitialContentSizeRatio

image.png

这个属性定义了宽和高的比例。当然设置了这个比例以后,也并不是万能的。因为你并不知道你会接受到多长的content。当你仅仅只设置比例,还是不能完整的展示所有的内容。有些时候如果我们可以知道最终的尺寸,那么我们固定尺寸会更好。

优化三:这时候我们发现我们自定义的界面显示的内容(custom content)和系统默认的内容(default content)重复了。

可以在 extensioninfo.plist 里设置,把系统默认的样式隐藏。增加字段UNNotificationExtensionDefaultContentHidden

image.png

将系统内容隐藏后效果如下:

image.png

1.6 自定义操作

iOS8开始引入的 action 的工作原理:

默认系统的 Action 的处理是,当用户点击的按钮,就把 action 传递给app,与此同时,推送通知会立即消失。这种做法很方便。

但是有的情况是,希望用户点击 action 按钮后,效果及时响应在我们自定义的UI上。这个时候,用户点击完按钮,我们把这个 action 直接传递给 extension ,而不是传递给app。当 actions 传递给 extension时,它可以延迟推送通知的消失时间。在这段延迟的时间之内,我们就可以处理用户点击按钮的事件了,并且更新UI,一切都处理完成之后,我们再去让推送通知消失掉。

这里我们可以运用 UNNotificationContentExtension 协议的第二个方法,这方法是Optional

- (void)didReceiveNotificationResponse:(UNNotificationResponse *)response completionHandler:(void (^)(UNNotificationContentExtensionResponseOption option))completion
{
    if ([response.actionIdentifier isEqualToString:@"action-like"]) {
        self.label.text = @"点赞成功~";
    }else if ([response.actionIdentifier isEqualToString:@"action-collect"]){
        self.label.text = @"收藏成功~";        
    }else if ([response.actionIdentifier isEqualToString:@"action-comment"]){
        self.label.text = [(UNTextInputNotificationResponse *)response userText];
    }
 
    //这里如果点击的action类型为UNNotificationActionOptionForeground,
    //则即使completion设置成Dismiss的,通知也不能消失
 
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        completion(UNNotificationContentExtensionResponseOptionDismiss);
    });
}

在这个方法里判断所有的action,更新界面,并延迟1.5秒后让通知消失。真实情况可能是,点击“赞”按钮后,发送请求给服务器,根据服务器返回结果,展示不同的UI效果在通知界面上,然后消失。如果是评论,则将评论内容更新到界面上。 如果还想把这个action传递给app,最后消失的参数应该这样:

completion(UNNotificationContentExtensionResponseOptionDismissAndForwardAction);

注意:如果点击的action类型为UNNotificationActionOptionForeground,则即使completion设置成Dismiss的,通知也不能消失,也没有启动app。

1.7 自定义输入型操作

action 有2种类型:

  • UNNotificationAction 普通按钮样式
  • UNTextInputNotificationAction 输入框样式

UNTextInputNotificationAction 的样式如下:

image.png

系统的输入样式的action,只有在点击发送按钮时,才能接受到action的响应回调。(比如上面的didReceiveNotificationResponse:completionHandler:方法)。但有的时候系统的样式或者功能不能满足需求,这时候可以自定义键盘上面的inputAccessoryView

首先,重写ViewController的下面两个方法:

- (BOOL)canBecomeFirstResponder
{
    return YES;
}
 
- (UIView *)inputAccessoryView
{
    return self.customInputView;
}

自定义inputAccessoryView,以绘制自定义的输入样式。

- (void)didReceiveNotificationResponse:(UNNotificationResponse *)response completionHandler:(void (^)(UNNotificationContentExtensionResponseOption option))completion
{
    ...
 
    }else if ([response.actionIdentifier isEqualToString:@"action-comment"]){
        self.label.text = [(UNTextInputNotificationResponse *)response userText];
        [self becomeFirstResponder];
        [self.textField becomeFirstResponder];
 
        self.completion = completion;
    }
}

实现了点击评论按钮,ViewController 成为第一响应者,使自定义的输入样式显示出来。然后,让 textField成为第一响应者,使键盘弹出。 这里将操作的completion保存,以便在需要的时候调用。比如,可以在点击键盘右下的send按钮时,调用completion,使通知消失。

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    [textField resignFirstResponder];
    self.label.text = textField.text;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.completion(UNNotificationContentExtensionResponseOptionDismiss);
    });
    return YES;
}

实现效果:

image.png

3. Push开关是否打开

Push功能完成后,我们一般会有判断App是否打开了通知开关的需求。如果用户没有打开可以提示用户再次打开,以保证Push消息能够推动给更多的用户,提高消息转化率。由于iOS10以上的通知相关API发生了较大变化,我们需要针对不同的系统版本使用不同的API来判断。具体代码如下:

+ (BOOL)isOpenNotificationSetting {
    __block BOOL isOpen = NO;
    if (@available(iOS 10.0, *)) { //iOS10及iOS10以上系统
       dispatch_semaphore_t semaphore;
       semaphore = dispatch_semaphore_create(0);
       // 异步方法,使用信号量的方式加锁保证能够同步拿到返回值
       [[UNUserNotificationCenter currentNotificationCenter] getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
           if (settings.authorizationStatus == UNAuthorizationStatusAuthorized) {
               isOpen = YES;
           }
           dispatch_semaphore_signal(semaphore);
       }];
       dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
   } else { 
       UIUserNotificationSettings *settings = [[UIApplication sharedApplication] currentUserNotificationSettings];
       if (settings.types != UIUserNotificationTypeNone) {
           isOpen = YES;
       }
   }
   return isOpen;
}

由于 getNotificationSettingsWithCompletionHandler: 方法是一个异步方法,如果直接在回调中去判断当前的push授权状态的话,还未给 isOpen 赋值就已经 return 返回结果了。

问题有了,那么解决方案也有很多,如代码中所示,我们使用了信号量 dispatch_semaphore ,功能类似于iOS开发中的锁(比如 NSLock@synchronize 等),它是一种基于计数器的多线程同步机制。

  • 使用 dispatch_semaphore_create 创建信号量 semaphore,此时信号量的值是0。
  • 异步跳过block块代码,执行 dispatch_semaphore_wait ,信号量的值减1。此时当前线程处于阻塞状态,等待信号量加1才能继续执行。
  • block块代码异步执行,dispatch_semaphore_signal 执行后发送一个信号,信号量的值加1,线程被唤醒,继续执行,给isOpen赋值,然后return,保证了每次能够正确取到当前的push开关状态。

4. 结合使用两个扩展

可以在content extension里面绘制界面时,通过notification.request.content.attachments获取附件放到自定义控件里面。

- (void)didReceiveNotification:(UNNotification *)notification {
 
    ...
 
    UNNotificationAttachment *attachment = notification.request.content.attachments.firstObject;
    if (attachment) {
        if ([attachment.URL startAccessingSecurityScopedResource]) {
            self.imageView.image = [UIImage imageWithContentsOfFile:attachment.URL.path];
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [attachment.URL stopAccessingSecurityScopedResource];
            });
        }
    }
}

我们可以提取contentattachments。前文提到过,attachment是由系统管理的,系统会把它们单独的管理,这意味着它们存储在我们sandbox之外。所以这里我们要使用attachment之前,我们需要告诉iOS系统,我们需要使用它,并且在使用完毕之后告诉系统我们使用完毕了。对应上述代码就是-startAccessingSecurityScopedResource-stopAccessingSecurityScopedResource的操作。当我们获取到了attachment的使用权之后,我们就可以使用那个文件获取我们想要的信息了。

参考:juejin.cn/post/740614…