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 模板。如下图所示:
点击Next后,你需要填写特定于应用程序的相关信息。添加完毕,点击Finish可以在项目的TARGETS里看到多了Service Extension一项。如图所示:
而项目中则会生成 NotificationService 文件夹,以及相应的类文件和plist文件,如图所示:
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秒触发本地通知,具体样式可以看下图
-
UNNotificationContent和UNMutableNotificationContent(通知内容和可变通知内容)
通知内容分为可变的以及不可变的两种类型,类似于可变数组跟不可变数组。后续我们通过某一特定标识符更新通知,便是用可变通知了。
不管是可变通知还是不可变通知,都有以下的几个属性:
// 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: ,在使用时,必须指定使用文件附件的内容,并且文件格式必须是支持的类型之一。创建附件后,将其分配给内容对象的附件属性。 (对于远程通知,您必须从您的服务扩展做到这一点。)
附件通知支持的类型如下图:
下面是创建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。
使用方法如下:
dict[UNNotificationAttachmentOptionsThumbnailHiddenKey] = @YES;
(3)UNNotificationAttachmentOptionsThumbnailClippingRectKey 剪贴矩形的缩略图。这个密钥的值是包含一个归一化的 CGRect - 也就是说,一个单元的矩形,其值是在以1.0〜 0.0 ,表示要显示的原始图像的所述部分的字典。例如,指定的(0.25 , 0.25)的原点和大小(0.5 ,0.5 )定义了剪辑矩形,只显示图像的中心部分。使用 CGRectCreateDictionaryRepresentation 函数来创建字典的矩形。
整张图被分割了,整体比例为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))));
使用上面的方法,可以得到一张图的阴影部分的图像,这张图像会是通知的缩略图。比如我下面的这个图,缩略图大家应该可以发现变了吧。
这里为了理解,在给大家说几个"坐标点": (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点都推送这个通知,只要repeats为YES就可以了。
// 周一早上 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(通知内容扩展)允许开发者加入自定义的界面,在这个界面里面,你可以绘制任何你想要的东西。但是有一个最重要的限制就是,这个自定义的界面没有交互。它们不能接受点击事件,用户并不能点击它们。但是推送通知还是可以继续与用户进行交互,因为用户可以使用 notificaiton 的 actions 。注意:extension 也可以处理这些 actions 。
1.1 推送界面的组成
- 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 模板。如下图所示:
创建新的Notification Content的target后。Xcode自动生成一个新的模板,下面有三个文件,ViewController、main Interface storyboard、info.plist。
然后打开这里的 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。这时候就需要配置 extension 的 info.plist 文件。
这里和我们给 notification actions 注册 category 一样,给这个通知扩展指定相应的 category。在 UNNotificationExtensionCategory 字段里写入相应的 category id。值得提到的一点是,这里对应的 category 是可以为一个数组的,里面可以为多个 category ,这样做的目的是多个 category 共用同一套UI。
上图中 category id 为 myNotificationCategory1 和 myNotificationCategory2 的通知就共用了一套UI。
设置了 category 后, 只要在通知里面增加 category 字段,值是上面在 extension 的 plist 里面配置的 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 中增加一些 Label 和 ImageView ,收到通知的时候,提取想要的内容,或者添加额外的内容,设置到我们自定义的 View 上。
1.5 UI界面优化
优化一:发现是自定义界面的大小很不美观
这时候可以通过设置 ViewController 的 preferredContentSize 大小,控制自定义视图的大小。
也可以通过约束,控制自定义视图的大小。
- (void)viewDidLoad {
[super viewDidLoad];
self.preferredContentSize = CGSizeMake(CGRectGetWidth(self.view.frame), 100);
}
优化二:目标大小的问题解决了,但是发现视图恢复成正确的尺寸前,先展示有一大片空白的样子,然后变成正确的样子。当通知展示出来之后,它的大小并不是正常的我们想要的尺寸。iOS系统会去做一个动画来Resize它的大小。这样体验很差。
会出现上面这张图的原因是,在推送送达的那一刻,iOS系统需要知道我们推送界面的最终大小。但是我们自定义的extension 在系统打算展示推送通知的那一刻,并还没有启动。所以这个时候,在我们代码都还没有跑起来之前,我们需要告诉iOS系统,我们的 View 最终要展示的大小。
为了解决这个问题,我们需要在 extension 的 info.plist 里设置一个 content size ratio 。增加字段UNNotificationExtensionInitialContentSizeRatio。
这个属性定义了宽和高的比例。当然设置了这个比例以后,也并不是万能的。因为你并不知道你会接受到多长的content。当你仅仅只设置比例,还是不能完整的展示所有的内容。有些时候如果我们可以知道最终的尺寸,那么我们固定尺寸会更好。
优化三:这时候我们发现我们自定义的界面显示的内容(custom content)和系统默认的内容(default content)重复了。
可以在 extension 的 info.plist 里设置,把系统默认的样式隐藏。增加字段UNNotificationExtensionDefaultContentHidden。
将系统内容隐藏后效果如下:
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 的样式如下:
系统的输入样式的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;
}
实现效果:
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];
});
}
}
}
我们可以提取content的attachments。前文提到过,attachment是由系统管理的,系统会把它们单独的管理,这意味着它们存储在我们sandbox之外。所以这里我们要使用attachment之前,我们需要告诉iOS系统,我们需要使用它,并且在使用完毕之后告诉系统我们使用完毕了。对应上述代码就是-startAccessingSecurityScopedResource和-stopAccessingSecurityScopedResource的操作。当我们获取到了attachment的使用权之后,我们就可以使用那个文件获取我们想要的信息了。