iOS 10 推送通知详解

iOS10推出之后,苹果对推送通知模块进行了比较彻底的重构,展现形态上,新的推送通知要要丰富很多,支持标题,子标题,内容,多媒体,另外也支持用交互,代码层面,推送通知统一由UserNotifications FrameWork 管理,统一了本地通知与远程通知的概念,统一了通知处理的回调入口,对于开发者来说是一大快事。
不过,现阶段我们肯定不能只用ios10 的新推送特性,所以未来一段时间,我们的代码里面可能需要两套推送通知的代码,根据系统的版本区分调用

请求权限

推送的所有操作都统一由UNUserNotificationCenter来处理
开始之前需要先导入#import <UserNotifications/UserNotifications.h>

请求推送通知权限

这里只是向用户请求推送通知的权限,如果用户允许,那么我们可以发送处理通知(本地通知以及远程通知),但是如果我们想发送远程通知,还必须要拿到token,这一步的注册并不包含拿token的注册,如果我们只调用了下面的方法,是不会进入token的回调的

1
2
3
4
5
6
7
8
[[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:UNAuthorizationOptionAlert|UNAuthorizationOptionBadge|UNAuthorizationOptionSound completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (granted) {
NSLog(@"Notification Enabled");
}else
{
NSLog(@"请求推送通知权限失败 %@",error);
}
}];

请求token

在ios10上请求获取token还是用的原来的方法,调用该方法后,会收到获取token成功或者失败的回调

1
[[UIApplication sharedApplication] registerForRemoteNotifications];

获取用户Notification的配置

我们可以获取用户对Notification的配置,包括,是否允许通知,开启通知的哪些功能等等

1
2
[[UNUserNotificationCenter currentNotificationCenter] getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
}];

新的通知

ios10对本地通知以及远程通知进行了统一,所以不论是本地通知,还是远程通知,我们拿到的对象都是一致的,接收到通知的处理流程也是一致的

不同的是本地通知我们是用代码构建的,远程通知是拼Json数据构建的

UNNotificationContent

新的通知内容对象,包括了一个推送通知内容方面的所有定制

1
2
3
4
5
6
7
8
9
10
11
12
//附件,图片,音频,等
@property (NS_NONATOMIC_IOSONLY, readonly, copy) NSArray <UNNotificationAttachment *> *attachments
@property (NS_NONATOMIC_IOSONLY, readonly, copy, nullable) NSNumber *badge;
@property (NS_NONATOMIC_IOSONLY, readonly, copy) NSString *body
@property (NS_NONATOMIC_IOSONLY, readonly, copy) NSString *categoryIdentifier;
//从通知开启app时的启动图
@property (NS_NONATOMIC_IOSONLY, readonly, copy) NSString *launchImageName
@property (NS_NONATOMIC_IOSONLY, readonly, copy, nullable) UNNotificationSound *sound;
@property (NS_NONATOMIC_IOSONLY, readonly, copy) NSString *subtitle;
@property (NS_NONATOMIC_IOSONLY, readonly, copy) NSString *threadIdentifier;
@property (NS_NONATOMIC_IOSONLY, readonly, copy) NSString *title;
@property (NS_NONATOMIC_IOSONLY, readonly, copy) NSDictionary *userInfo;

对比原来的推送内容,增加了title,subtitle,多媒体attachment支持等等。

UNNotificationAttachment

表示一个多媒体类型附件,目前测试支持图片,音频,视频
必须使用本地资源,如果资源文件名字包含扩展名,那么不需要显示的指定文件类型,如果资源文件没有扩展名,那么需要通过option的UNNotificationAttachmentOptionsTypeHintKeykey指定资源的**UTI**类型,否则会创建失败

UTI讲解

1
2
3
4
5
6
7
8
9
10
11
//图片  路径不指定类型,通过option指定UTI类型
NSString *path= @"/Document/xxxxxxxx/xxxxxx";
UNNotificationAttachment *attachMent=[UNNotificationAttachment attachmentWithIdentifier:@"aabbcc" URL:[NSURL fileURLWithPath:path] options:@{UNNotificationAttachmentOptionsTypeHintKey:@"public.jpeg"} error:&errr];

//音频
NSString *path= [[NSBundle mainBundle] pathForResource:@"test2" ofType:@"mp3"];
UNNotificationAttachment *attachMent=[UNNotificationAttachment attachmentWithIdentifier:@"aabbcc" URL:[NSURL fileURLWithPath:path] options:nil error:&errr];

//视频
NSString *path= [[NSBundle mainBundle] pathForResource:@"test3" ofType:@"mp4"];
UNNotificationAttachment *attachMent=[UNNotificationAttachment attachmentWithIdentifier:@"aabbcc" URL:[NSURL fileURLWithPath:path] options:nil error:&errr];

注意,这种代码创建的方式只适用于本地通知,如果远程通知我们也想添加Attachment,那么必须依赖Notification Service Extension,原理是在远程通知显示之前,修改通知的UNNotificationContent对象,然后为其添加Attachment,后面会讲到

UserInfo

其中userInfo对象,在本地通知的时候由我们创建并填入自定义信息。

在远程通知的时候,是推送通知的整个json字符串的Dictionary对象,比如下面这种

1
2
3
4
5
6
7
8
9
10
11
12
{
"aps": {
"alert": {
"body": "this is body",
"title": "this is title",
},
"badge": 4,
"payload": "payload"
}
"key1":"aaaaaa",
"key2":"bbbbbb"
}

所以远程推送的时候,我们可以通过userInfo[@"key1"]这种方式拿到我们的自定义数据

一个完整的content对象

下面构建一个推送通知内容对象

1
2
3
4
5
6
7
8
9
10
UNMutableNotificationContent *content=[[UNMutableNotificationContent alloc]init];
content.title=@"this is ios 10 title";
content.body=@"this is ios 10 body";
content.subtitle=@"this is ios 10 subtitle";
content.badge=@100;
//添加一个图片,图片必须使用本地资源,并且必须携带后缀名,如果没有携带后缀名,需要在
//option里面指定
NSString *path= [[NSBundle mainBundle] pathForResource:@"image" ofType:@"jpg"];
UNNotificationAttachment *attachMent=[UNNotificationAttachment attachmentWithIdentifier:@"aabbcc" URL:[NSURL fileURLWithPath:path] options:nil error:&errr];
content.attachments=@[attachMent];

通过上面的代码创建了一个content对象,设置了title,subTitle,body,以及一张图片,需要注意的是图片必须使用本地资源,并且必须携带后缀名,如果没有携带后缀名,需要在
option里面通过UNNotificationAttachmentOptionsTypeHintKeykey指定类型

Trigger触发器

本地推送通知需要设置一个触发器,当触发器满足条件的时候,发起本地推送,包括如下三种

1
2
3
4
5
6
//时间触发器
UNTimeIntervalNotificationTrigger
//日历触发器
UNCalendarNotificationTrigger
//位置触发器
UNLocationNotificationTrigger

创建触发器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//5秒以后触发,不重复
UNTimeIntervalNotificationTrigger *timeTrigger=[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:5 repeats:NO];

//72110点出发,不重复
NSDateComponents *components = [[NSDateComponents alloc] init];
components = 2016;
components.month = 7;
components.day=21;
components.hour =10;
UNCalendarNotificationTrigger *dateTrigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:NO];

//圆形区域,进入时候进行触发,退出的时候触发
CLLocationCoordinate2D center = CLLocationCoordinate2DMake(33.221400, -115.022311);
CLCircularRegion* region = [[CLCircularRegion alloc] initWithCenter:center
radius:100.0 identifier:@"identifier"];
region.notifyOnEntry = YES;
region.notifyOnExit = YES;
UNLocationNotificationTrigger* locationTrigger = [UNLocationNotificationTrigger
triggerWithRegion:region repeats:NO];

推送请求

这里面有点像网络编程的概念,上面创建了content,trigger,需要用其创建一个request,其中比较重要的是identifier属性,我们可以通过这个标识符来取消,更新未发送的通知

1
2
NSString *identifider=@"notification1";
UNNotificationRequest *request=[UNNotificationRequest requestWithIdentifier:identifider content:content trigger:trigger];

添加请求

请求创建完毕需要添加到队列,发送请求,这样,一个本地通知就创建完成,等待触发时机进行推送

1
2
3
4
5
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
if (error) {

}
}];

推送通知的生命周期

查询,更新,删除

当我们把一个推送通知request添加到UNUserNotificationCenter之后,就会等待其触发,现在iOS10之后,在其触发之前,或者触发之后,我们可以对其进行如下一系列操作。

  • 获取挂起的所有通知请求
  • 删除挂起的请求(使用标识符),这样改请求就不会再触发
  • 删除所有挂起的请求
  • 获取已经送达的通知(没有被用户点击,仍然保存在通知中心列表的所有通知)
  • 删除已经送达的通知(使用标识符)
  • 删除已经送达的所有通知
  • 更新上面的通知,也会直接反应到系统的通知中心
1
2
3
4
5
6
7
8
9
//挂起的通知操作
- (void)getPendingNotificationRequestsWithCompletionHandler:(void(^)(NSArray<UNNotificationRequest *> *requests))completionHandler;
- (void)removePendingNotificationRequestsWithIdentifiers:(NSArray<NSString *> *)identifiers;
- (void)removeAllPendingNotificationRequests;

//已经送达的通知操作
- (void)getDeliveredNotificationsWithCompletionHandler:(void(^)(NSArray<UNNotification *> *notifications))completionHandler;
- (void)removeDeliveredNotificationsWithIdentifiers:(NSArray<NSString *> *)identifiers;
- (void)removeAllDeliveredNotifications;

接收回调

不同于之前的ios版本,把本地通知,远程通知,点击通知的,处理入口分散到各个地方,ios10将所有通知的处理回调统一起来,主要集中在UNUserNotificationCenter的delegate的两个函数

1
2
3
4
5
//当应用在前台的时候,收到推送通知,在展示之前会进入这个方法,我们通过设定options来决定如何展示这条通知,可以是只展示badge,sound,alert,也可都展示
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler;

//当用户对通知做出响应的时候,进入这个方法,一般是点击了通知,或者点击了通知的action进行交互
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler;

在ios10以前,当应用在前台的时候,即使收到通知,也不会展示在顶部,如果想展示我们只能自己实现UI,在ios10以后,当应用在前台的时候,我们只需要实现willPresentNotification方法,就能决定是否展示这个通知。当然我们可以根据通知的content做决定

当用户点击通知的时候,无论是本地通知,还是远程通知,无论应用是打开还是关闭,肯定会进入didReceiveNotificationResponse方法,我们可以在这里进行统一的处理

远程推送

远程推送需要服务端构建推送json结构,下面是一个不带任何多媒体信息的推送结构

1
2
3
4
5
6
7
8
9
10
11
12
{
"aps": {
"alert": {
"body": "this is body",
"title": "this is title",
"subtitle":"this is subtitle"
},
"badge": 4,
}
"key1":"aaaaaa",
"key2":"bbbbbb"
}

如果我们想要发送一个远程通知,并且携带多媒体信息,我们需要在aps里面增加mutable-content : 1,用来表示这个推送的content是可以被修改的,这样,当我们为app创建了notification service extension之后,会进入notification service extension的回调方法,在里面,我们可以在远程通知展示之前,对其content进行修改。

依照这个原理,我们需要构建如下json

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"aps": {
"alert": {
"body": "this is body",
"title": "this is title",
"subtitle":"this is subtitle"
},
"badge": 4,
"mutable-content" : 1
}
"image":"http://www.xxxx.xxx/xxxxxx.jpg",
"key2":"bbbbbb"
}

然后在notification service extension的回调方法里面通过userInfo获取image,进行下载,然后用下载的本地文件创建attachment,最后赋值给content,即可完成对远程通知的修改。这样就能展示多媒体内容

Notification Service Extension

Notification Service Extension 是一个app extension,是一段嵌入宿主app的二进制文件。

Notification Service Extension允许我们在收到远程推送并且在其展示之前,对推送的内容进行修改。

首先我们要创建一个Notification Service Extension target

其中Embed in Application 是我们Notification Service Extension需要嵌入的程序,也就是说,我们只对这个application起作用

创建好Notification Service Extension之后,系统会自动为我们创建如下文件

其中主要包含两个方法

1
2
3
4
5
//收到远程通知,在展示之前会进入这个方法。这里允许我们创建一个新的content对象,返回给contentHandler,用来展示
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler;

//加入上面的方法耗时比较长,在一定的时间内都没创建并返回一个content,就会调用这个过期方法,在这里我们可以最终返回一个可以使用的content。
- (void)serviceExtensionTimeWillExpire;

下面的代码是一个展示图片的Notification Service Extension代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];


NSString *url = [NSString stringWithFormat:@"%@",request.content.userInfo[@"image"]];

NSURLSessionDataTask *task= [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:url] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *docDir = [paths objectAtIndex:0];
NSString *path= [docDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@",[self md5String:url]]];
[data writeToFile:path atomically:YES];


NSError *attachError;
UNNotificationAttachment *attachMent=[UNNotificationAttachment attachmentWithIdentifier:@"aa" URL:[NSURL fileURLWithPath:path] options:@{UNNotificationAttachmentOptionsTypeHintKey:@"public.jpeg"} error:&attachError];
self.bestAttemptContent.attachments=@[attachMent];
self.contentHandler(self.bestAttemptContent);
}];

[task resume];
}

- (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);
}

Notification Service Extension 其实也是一个独立的二进制文件,也有info.plist配置文件,也有对应的bundleId,同样需要用证书签名,需要对应的mobileprovision配置文件。

打开编译好的app包,发现里面多了Plugins文件夹子,里面正式这个app所有的extension包

右键显示包内容,其内容与普通的app包是一样的

所以如果我们想对app重签名,必须对Plugins文件夹内的所有extension进行重签名,而对extension重签名,同样需要知道其bundleId,需要为这个bundleId创建mobileprovision配置文件

另外我们运行的时候选择extension这个target然后从选择框里面选择embeded application,这样当我们收到推送的时候,能在didReceiveNotificationRequest里面进行断点调试