APNs(Apple Push Notification service-苹果推送通知服务)
APNs官方文档
APNs是推送的核心。该服务与iOS设备建立起强大的持久连接通讯(和间接WatchOS,TVOS,和MacOS设备)。在早期的时候,iOS通过管理AppSSL认证的推送证书与APNs建立起长连接通讯,但不是可靠的通讯。随后,APNs使用持久连接进行服务端推送。在长期的演进过程中,现在iOS10提供的APNs服务是基于HTTP/2协议栈同时使用Json Web Token(json令牌)保证通讯安全。有如下几项改进:
iOS 8以后,APNs推送的字节是2k,iOS8以前是256字节,iOS10现在是4k
iOS 9以后APNs支持HTTP/2协议栈,优化长连接,具有标准的HTTP返回和管道复用技术
iOS 10以后,APNs可根据推送消息的唯一标示符查询某条消息是否被用户阅读,可更新某一推送消息,而不用发重读的多条消息
更多的详细解释有一篇文章叫做 《国内 90%以上的 iOS 开发者,对 APNs 的认识都是错的》,大家可以深入了解。
通知
iOS操作系统的通知包括了App的通知、系统的通知和官方应用的通知,实质上就是推送的数据在iOS操作系统上的表现和本地通知在iOS操作系统的表现,在交互上iOS10的通知大大增强,可定制化UI,增加了更加细分的通知权限管理和更多的通知设定,例如远程通知、时间通知、地理位置通知和日历通知。
很多开发者都知道iOS10中苹果升级推出了 User Notifications Framework与 User Notifications UI Framework两个框架,但是千万不要跟推送混为一谈,这两个框架升级和打包的是通知的功能增加和通知交互层面上的改进。
推送Push只不过是iOS10通知的一种触发器。
配合最新的推送服务使用强大的iOS10通知功能
重点介绍一下iOS10的通知新功能,用户体验的提升和开发者能够发挥的地方非常多,使得iOS更具有竞争力。
iOS 10通知系统支持Images, GIFs, Audio and Video类型
iOS 10推出Notification Service Extension与Notification Content Extension,可以实现推送数据在展示前进行下载更新、定制通知UI
iOS 10统一了通知类型,具有时间间隔通知、地理位置通知和日历通知
iOS里的通知扩展
User Notifications Framework 介绍:
关系图:
User Notifications Framework类关系图
重点介绍:
UNUserNotificationCenter通知中心,用以管理通知的注册、权限获取和管理、通知的删除与更新,通过代理分发事件等。
UNNotification 通知实体,在UNUserNotificationCenter的代理回调事件中,告知App接收到一条通知,包含一个发起通知的请求UNNotificationRequest
UNNotificationRequest包含通知内容UNNotificationContent和触发器UNNotificationTrigger
UNNotificationContent 通知内容,通知的title,sound,badge以及相关的图像、声音、视频附件UNNotificationAttachment,触发打开App时候指定的LacnchImage等
UNNotificationResponse,用户在触发了按钮或者文本提交的UNNotificationAction的时候,会形成一个response,通过通知中心的代理方法回调给App进行处理或者是交给扩展处理。
UNNotificationServiceExtension,是一个在接收到APNs服务器推送过来的数据进行处理的服务扩展,如果App提供了服务扩展,那么APNs下发推送后在通知显示触发之前,会在UNNotificationServiceExtension内接收到,此处有大约30秒的处理时间,开发者可以进行一些数据下载、数据解密、更新等操作,然后交由而后的内容扩展(UNNotificationContentExtension)或者是App进行触发显示
UNNotificationCategory,用以定义一组样式类型,该分类包含了某一个通知包含的交互动作的组合,比如说UNNotificationRequest内包含了一个Category标示,那该通知就会以预定义好的交互按钮或者文本框添加到通知实体上。
UNNotificationAttachment,通知内容UNNotificationContent包含的附件,一般为图片、视频和音频,虽然iOS10的通知数据容量为4k,但依旧很少,在添加了UNNotificationServiceExtension扩展的情况下,可以在服务里下载图片,生成图片、视频等的本地缓存,UNNotificationAttachment根据缓存数据生成并添加到UNNotificationContent中,交由UI显示
UNNotificationAction,是通知中添加的action,展示在通知栏的下方。默认以的button样式展示。有一个文本输入的子类UNTextInputNotificationAction。可以在点击button之后弹出一个键盘,输入信息。用户点击信息和输入的信息可以在UNNotificationResponse中获取
User Notifications UI Framework介绍:
关系图:
User Notifications UI Framework类关系图
10.?UNNotificationContentExtension<协议>,NotificationViewController实现该协议,可以获得iOS展示自定义UI时候分发的UNNotification对象和用户交互的Response
11.NotificationViewController,App添加Notification Content Extension扩展的时候,自动生成的Controller,可以定义通知UI的主题部分,由StoryBoard指定设计
iOS10注册通知、回调处理
只简单介绍一下iOS10的通知注册
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
// 必须写代理,不然无法监听通知的接收与点击
center.delegate = self;
//设置预设好的交互类型,NSSet里面是设置好的UNNotificationCategory
[center setNotificationCategories:[self createNotificationCategoryActions]];
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
if
(settings.authorizationStatus==UNAuthorizationStatusNotDetermined) {
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionSound) completionHandler:^(BOOL granted, NSError * _Nullable error) {
if
(granted) {
}
else
{
}
}];
}
else
{
//do other things
}
}];
- (void)application:(UIApplication *)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
//上传token
}
- (void)application:(UIApplication *)application
didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
//获取token失败,开发调试的时候需要关注,必要的情况下将其上传到异常统计
}
//代理回调方法,通知即将展示的时候
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler{
UNNotificationRequest *request = notification.request;
// 原始请求
NSDictionary * userInfo = notification.request.content.userInfo;
//userInfo数据
UNNotificationContent *content = request.content;
// 原始内容
NSString *title = content.title;
// 标题
NSString *subtitle = content.subtitle;
// 副标题
NSNumber *badge = content.badge;
// 角标
NSString *body = content.body;
// 推送消息体
UNNotificationSound *sound = content.sound;
// 指定的声音
//建议将根据Notification进行处理的逻辑统一封装,后期可在Extension中复用~
completionHandler(UNNotificationPresentationOptionBadge|UNNotificationPresentationOptionSound|UNNotificationPresentationOptionAlert);
// 回调block,将设置传入
}
//用户与通知进行交互后的response,比如说用户直接点开通知打开App、用户点击通知的按钮或者进行输入文本框的文本
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler{
UNNotificationRequest *request = response.notification.request;
// 原始请求
NSDictionary * userInfo = notification.request.content.userInfo;
//userInfo数据
UNNotificationContent *content = request.content;
// 原始内容
NSString *title = content.title;
// 标题
NSString *subtitle = content.subtitle;
// 副标题
NSNumber *badge = content.badge;
// 角标
NSString *body = content.body;
// 推送消息体
UNNotificationSound *sound = content.sound;
//在此,可判断response的种类和request的触发器是什么,可根据远程通知和本地通知分别处理,再根据action进行后续回调
}
|
以上就是iOS10的通知中心注册和设置管理的过程,一下还有一些比较有用API:
1
2
3
4
5
6
7
8
9
10
|
//获取在Pending状态下待触发的通知
- (void)getPendingNotificationRequestsWithCompletionHandler:(void(^)(NSArray *requests))completionHandler;
//移除未触发的通知
- (void)removePendingNotificationRequestsWithIdentifiers:(NSArray *)identifiers;
- (void)removeAllPendingNotificationRequests;
// 通知已经触发,但是还在操作系统的通知中心上,可以进行查询和删除
- (void)getDeliveredNotificationsWithCompletionHandler:(void(^)(NSArray *notifications))completionHandler __TVOS_PROHIBITED;
- (void)removeDeliveredNotificationsWithIdentifiers:(NSArray *)identifiers __TVOS_PROHIBITED;
- (void)removeAllDeliveredNotifications __TVOS_PROHIBITED;
|
iOS10本地通知
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
|
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = @
"\"Fly to the moon\""
;
content.subtitle = @
"by Neo"
;
content.body = @
"the wonderful song with you~"
;
content.badge = @0;
NSString *path = [[NSBundle mainBundle] pathForResource:@
"image1"
ofType:@
"png"
];
NSError *error = nil;
//将本地图片的路径形成一个图片附件,加入到content中
UNNotificationAttachment *img_attachment = [UNNotificationAttachment attachmentWithIdentifier:@
"att1"
URL:[NSURL fileURLWithPath:path] options:nil error:&error];
if
(error) {
NSLog(@
"%@"
, error);
}
content.attachments = @[img_attachment];
//设置为@""以后,进入app将没有启动页
content.launchImageName = @
""
;
UNNotificationSound *sound = [UNNotificationSound defaultSound];
content.sound = sound;
//设置时间间隔的触发器
UNTimeIntervalNotificationTrigger *time_trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:10 repeats:NO];
NSString *requestIdentifer = @
"time interval request"
;
content.categoryIdentifier = @
""
;
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:requestIdentifer content:content trigger:time_trigger];
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
NSLog(@
"%@"
,error);
}];
|
这里面的图片附件后面再述,过程上能感受到,通知数据部分在UNMutableNotificationContent中设置,附件UNNotificationAttachment也是在其中包含,categoryIdentifier为指定该通知对应的交互样式,也就是前面设置的UNNotificationCategory的对象,后面再述。然后创建触发器,UNTimeIntervalNotificationTrigger,触发器有很多种,UNNotificationTrigger有四个子类:
UNPushNotificationTrigger,远程推送触发器,一般是远程推送推过来的通知带有这类触发器
UNTimeIntervalNotificationTrigger,时间间隔触发器,定时或者是重复,在本地推送设置中有用
UNCalendarNotificationTrigger,日历触发器,指定日期进行通知
UNLocationNotificationTrigger,地理位置触发器,指定触发通知的条件是地理位置CLRegion这个类型。
触发器和内容最后形成UNNotificationRequest,一个通知请求,本地通知的请求,直接交给通知中心进行发送,发送成功后,该通知会按照触发器的触发条件进行触发,并且会显示到通知中心上,用户可与指定的category交互方式与通知进行交互
如下图:
localTimeNotification.gif
iOS10远程通知
远程通知与本地通知的流程一样,只不过触发器是UNPushNotificationTrigger,并且不需要形成request,又Provider Service发送给APNs到iOS以后生成,在代理回调的函数中获取request
通知的代理回调
上面代码有些代理回调函数,可以在这两个代理回调函数里做一些事情
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler{
//该回调函数是在通知条即将显示之前调用的
if
([request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
//远程通知处理
}
if
([request.trigger isKindOfClass:[UNTimeIntervalNotificationTrigger class]]) {
//时间间隔通知处理
}
if
() {
//加解密,数据下载,完成后调用completionHandler
}
else
{
completionHandler(UNNotificationPresentationOptionBadge|UNNotificationPresentationOptionSound|UNNotificationPresentationOptionAlert);
}
}
|
如果你的App在前台,一般在这个回调函数里做一些数据加解密、数据下载,然后将下载的数据组装成UNNotificationAttachment或者是根据通知里面的content里面的userinfo里与后端服务约定好的修改通知对应的categoryId,调用相应的交互组件到通知上,completionHandler在你想要做的逻辑完成以后调用。
如果App在前台,你接收到通知,不想显示系统提示框,想使用App 自定义的通知消息弹窗,可以在completionHandler回调的时候传入的opinion不要带上UNAuthorizationOptionAlert,然后直接弹自定义的弹窗就Ok。
注意:改回调函数仅仅用来处理数据和重新选择交互方式,其他远程推送到达设备要做的业务逻辑,最好不要在此回调函数触发,保持职责单一
1
2
3
4
5
6
7
8
9
10
11
|
//用户与通知进行交互后的response,比如说用户直接点开通知打开App、用户点击通知的按钮或者进行输入文本框的文本
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler{
//在此,可判断response的种类和request的触发器是什么,可根据远程通知和本地通知分别处理,再根据action进行后续回调
if
([response.actionIdentifier isEqualToString:@
""
]) {
}
//也可根据response 判断是否是text文本输入
if
([response isKindOfClass:[UNTextInputNotificationResponse class]]) {
//该函数是在用户点击通知或者是与通知上面指定好的action进行了交互回调的函数,用户触发通知的业务逻辑最好放在此处
}
}
|
iOS10通知交互,UNNotificationAction与UNNotificationCategory
组合按钮
组合文本框
首先说明的是,iOS10通知上的交互只有两种,一种是Button一种是text,就算使用了iOS10 Notification Content Extension也不能添加自定义的按钮或者其他交互组件,因为不会响应。
1
2
3
4
5
6
7
8
9
10
11
|
-(NSSet *)createNotificationCategoryActions{
//定义按钮的交互button action
UNNotificationAction * likeButton = [UNNotificationAction actionWithIdentifier:@
"see1"
title:@
"I love it~"
options:UNNotificationActionOptionAuthenticationRequired|UNNotificationActionOptionDestructive|UNNotificationActionOptionForeground];
UNNotificationAction * dislikeButton = [UNNotificationAction actionWithIdentifier:@
"see2"
title:@
"I don't care~"
options:UNNotificationActionOptionAuthenticationRequired|UNNotificationActionOptionDestructive|UNNotificationActionOptionForeground];
//定义文本框的action
UNTextInputNotificationAction * text = [UNTextInputNotificationAction actionWithIdentifier:@
"text"
title:@
"How about it~?"
options:UNNotificationActionOptionAuthenticationRequired|UNNotificationActionOptionDestructive|UNNotificationActionOptionForeground];
//将这些action带入category
UNNotificationCategory * choseCategory = [UNNotificationCategory categoryWithIdentifier:@
"seeCategory"
actions:@[likeButton,dislikeButton] intentIdentifiers:@[@
"see1"
,@
"see2"
] options:UNNotificationCategoryOptionNone];
UNNotificationCategory * comment = [UNNotificationCategory categoryWithIdentifier:@
"seeCategory1"
actions:@[text] intentIdentifiers:@[@
"text"
] options:UNNotificationCategoryOptionNone];
return
[NSSet setWithObjects:choseCategory,comment,nil];
}
|
在上面封装了上面两张图中所示的两个category组合,每一个category携带的action如图所示,在通知中心初始化的时候设置app要支持的category。
1
2
|
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center setNotificationCategories:[self createNotificationCategoryActions]];
|
UNNotificationAction 在初始化的时候需要定义UNNotificationActionOptions,这个UNNotificationActionOptions的意思是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
typedef NS_OPTIONS(NSUInteger, UNNotificationActionOptions) {
// Whether this action should require unlocking before being performed.
//指定该动作是否需要用户解锁验证身份
UNNotificationActionOptionAuthenticationRequired = (1 << 0),
// Whether this action should be indicated as destructive.
//指定用户执行该动作是否要将通知从iOS的通知中心移除,以防止处理过该通知以后重复处理
UNNotificationActionOptionDestructive = (1 << 1),
// Whether this action should cause the application to launch in the foreground.
//指定通知action点击后是否要进入app到前台,如果到前台,这个对Notification Content Extension的自定义的通知UI有意义,
//可以在Extension中处理用户的点击或者提交文字,那么就可以指定该action不需要进入app,
//UNNotificationActionOptionAuthenticationRequired这个就不要加入
UNNotificationActionOptionForeground = (1 << 2),
} __IOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0) __TVOS_PROHIBITED;
|
测试我们预设好的category
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
29
|
-(void)timeLoacl{
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = @
"\"Fly to the moon\""
;
content.subtitle = @
"by Neo"
;
content.body = @
"the wonderful song with you~"
;
content.badge = @0;
NSString *path = [[NSBundle mainBundle] pathForResource:@
"image1"
ofType:@
"png"
];
NSError *error = nil;
UNNotificationAttachment *img_attachment = [UNNotificationAttachment attachmentWithIdentifier:@
"att1"
URL:[NSURL fileURLWithPath:path] options:nil error:&error];
if
(error) {
NSLog(@
"%@"
, error);
}
content.attachments = @[img_attachment];
//设置为@""以后,进入app将没有启动页
content.launchImageName = @
""
;
UNNotificationSound *sound = [UNNotificationSound defaultSound];
content.sound = sound;
UNTimeIntervalNotificationTrigger *time_trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:5 repeats:NO];
NSString *requestIdentifer = @
"time interval request"
;
//在此指定通知内容的categoryIdentifier,就是上面我们预设好的category,一个category代表一种交互组合类型
content.categoryIdentifier = @
"seeCategory1"
;
// content.categoryIdentifier = @"seeCategory";
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:requestIdentifer content:content trigger:time_trigger];
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
NSLog(@
"%@"
,error);
}];
}
|
在点击某个按钮或者是输入了文本后,会在通知中心的代理回调函数里处理交互的response
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler{
//在此,可判断response的种类和request的触发器是什么,可根据远程通知和本地通知分别处理,再根据action进行后续回调
if
([response isKindOfClass:[UNTextInputNotificationResponse class]]) {
UNTextInputNotificationResponse * textResponse = (UNTextInputNotificationResponse*)response;
NSString * text = textResponse.userText;
//do something
}
else
{
if
([response.actionIdentifier isEqualToString:@
"see1"
]) {
//I love it~的处理
}
if
([response.actionIdentifier isEqualToString:@
"see2"
]) {
//I don't care~
[[UNUserNotificationCenter currentNotificationCenter] removeDeliveredNotificationsWithIdentifiers:@[response.notification.request.identifier]];
}
}
completionHandler();
}
|
这里需要根据response的类型或者根据actionIdentifier来区分用户的交互结果来处理逻辑
iOS10通知附件UNNotificationAttachment,展示图片、Gif、Audio和Video
gif通知
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
|
-(void)timeLoaclWithImage{
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = @
"\"Fly to the moon\""
;
content.subtitle = @
"by Neo"
;
content.body = @
"the wonderful song with you~"
;
content.badge = @0;
NSString *path = [[NSBundle mainBundle] pathForResource:@
"image1"
ofType:@
"png"
];
NSError *error = nil;
UNNotificationAttachment *img_attachment = [UNNotificationAttachment attachmentWithIdentifier:@
"att1"
URL:[NSURL fileURLWithPath:path] options:nil error:&error];
if
(error) {
NSLog(@
"%@"
, error);
}
content.attachments = @[img_attachment];
//设置为@""以后,进入app将没有启动页
content.launchImageName = @
""
;
UNNotificationSound *sound = [UNNotificationSound defaultSound];
content.sound = sound;
UNTimeIntervalNotificationTrigger *time_trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:5 repeats:NO];
NSString *requestIdentifer = @
"time interval request"
;
content.categoryIdentifier = @
"seeCategory1"
;
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:requestIdentifer content:content trigger:time_trigger];
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
NSLog(@
"%@"
,error);
}];
}
|
UNNotificationAttachment需要指定image、gif、audio与video的文件路径,
+ (nullable instancetype)attachmentWithIdentifier:(NSString *)identifier URL:(NSURL *)URL options:(nullable NSDictionary *)options error:(NSError *__nullable *__nullable)error;
此处有一个options的字典,传入的key有一下几点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// Key to manually provide a type hint for the attachment. If not set the type hint will be guessed from the attachment's file extension. Value must be an NSString.
extern NSString * const
//指定文件类型,查看文档可以发现支持哪些文件
UNNotificationAttachmentOptionsTypeHintKey __IOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0);
// Key to specify if the thumbnail for this attachment is hidden. Defaults to NO. Value must be a boolean NSNumber.
extern NSString * const
//指定通知上是否显示文件的缩略图
UNNotificationAttachmentOptionsThumbnailHiddenKey __IOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0);
// Key to specify a normalized clipping rectangle to use for the attachment thumbnail. Value must be a CGRect encoded using CGRectCreateDictionaryRepresentation.
//指定缩略图的切割比例
extern NSString * const UNNotificationAttachmentOptionsThumbnailClippingRectKey __IOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0);
// Key to specify the animated image frame number or the movie time to use as the thumbnail.
// An animated image frame number must be an NSNumber. A movie time must either be an NSNumber with the time in seconds or a CMTime encoded using CMTimeCopyAsDictionary.
extern NSString * const
//影片切割时间
UNNotificationAttachmentOptionsThumbnailTimeKey __IOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0);
|
从网上获取gif下载后展示
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
29
30
31
32
33
34
35
36
37
|
-(void)timeLoaclWithGif{
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@
"http://ww3.sinaimg.cn/large/006y8lVagw1faknzht671g30b408c1l2.gif"
] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if
(!error) {
//缓存到tmp文件夹
NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@
"tmp/%@att.%@"
,@([NSDate date].timeIntervalSince1970),@
"gif"
]];
NSError *err = nil;
[data writeToFile:path atomically:YES];
UNNotificationAttachment *gif_attachment = [UNNotificationAttachment attachmentWithIdentifier:@
"attachment"
URL:[NSURL fileURLWithPath:path] options:@{UNNotificationAttachmentOptionsThumbnailClippingRectKey:[NSValue valueWithCGRect:CGRectMake(0, 0, 1, 1)]} error:&err];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = @
"\"Fly to the moon\""
;
content.subtitle = @
"by Neo"
;
content.body = @
"the wonderful song with you~"
;
content.badge = @0;
NSError *error = nil;
if
(gif_attachment) {
content.attachments = @[gif_attachment];
}
if
(error) {
NSLog(@
"%@"
, error);
}
//设置为@""以后,进入app将没有启动页
content.launchImageName = @
""
;
UNNotificationSound *sound = [UNNotificationSound defaultSound];
content.sound = sound;
UNTimeIntervalNotificationTrigger *time_trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1 repeats:NO];
NSString *requestIdentifer = @
"time interval request"
;
content.categoryIdentifier = @
"seeCategory1"
;
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:requestIdentifer content:content trigger:time_trigger];
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
NSLog(@
"%@"
,error);
}];
}
}];
[task resume];
}
|
在文件缓存以后,发起本地通知。值得注意的一点是,形成request发起以后,如果URL所代表的文件过大,打开通知的交互界面的时候会非常慢,甚至有时候会出现资源显示不出来,还有一点是,当你在通知触发展示以后,再通过request取出attachment文件的URL的时候,发现URL竟然发生了变化,文件是缓存到一个叫pushstore的文件夹下,这个在后面介绍 Notification Service Extension与Notification Content Extension 数据共享的时候会讨论该问题。
iOS10 Notification Service Extension:
Notification Service Extension是Xcode8中加入众多extension的其中一种,Extension实际上是App提供了一个额外插件功能,以供iOS操作系统调用,与App是宿主关系。
Notification Service Extension target
工作流程如下:
Notification Service Extension流程
Notification Service Extension的作用:
使得推送的数据在iOS系统展示之前,经过App开发者的Extension,可以在不启动App的情况下,完成一些快捷操作逻辑,比如上面的例子,如果你是个社交App,可以在不启动App的情况下,直接点赞回复,而不用打开App,提高效率
虽然iOS10的推送数据包已经达到4k,但是对于一些图片视频gif还是无力的,有了Extension,可以在此下载完毕然后直接展示,丰富的图片和视频可以在此显示
可以在此Extension中如果要完成1中所述的用户行为操作,则必须加强安全性,服务端可以对推送的数据配合RSA算法用服务端的私钥加密,在Extension中使用服务端私钥解密,其实APNs从SSL数字安全证书到Json Web Token令牌,已经非常安全,但是大量的App使用第三方诸如JPush的推送服务,来跟APNs交互,业务数据跑在别人的管道上,当然有所顾忌,所以,这个地方加密的更多现实意义是防止业务数据被第三方服务商窥探。
新建一个target
addtarget_notification_service_extension
这点没什么好说的,BundleID 就是宿主App的BundleID.这里设置的ProductName ,自动生成。
注意使用组织名与team证书。
在新生成的NotificationService文件里有如下方法
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
-(void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
self.bestAttemptContent.title =@
""
;
// Modify the notification content here...
NSDictionary * userInfo = request.content.userInfo;
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
//服务端与客户端约定各种资源的url,根据url资源进行下载
NSString * imageUrl = [userInfo objectForKey:@
"imageUrl"
];
NSString * gifUrl = [userInfo objectForKey:@
"gifUrl"
];
NSString * typeString ;
NSURL * url;
if
(imageUrl.length>0) {
url = [NSURL URLWithString:imageUrl];
typeString = @
"jpg"
;
}
if
(gifUrl.length>0) {
url = [NSURL URLWithString:gifUrl];
typeString = @
"gif"
;
}
if
(url) {
NSURLRequest * urlRequest = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:5];
//注意使用DownloadTask,这点会详细说明
NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:urlRequest completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if
(!error) {
NSString *path = [location.path stringByAppendingString:[NSString stringWithFormat:@
".%@"
,typeString]];
NSError *err = nil;
NSURL * pathUrl = [NSURL fileURLWithPath:path];
[[NSFileManager defaultManager] moveItemAtURL:location toURL:pathUrl error:nil];
//下载完毕生成附件,添加到内容中
UNNotificationAttachment *resource_attachment = [UNNotificationAttachment attachmentWithIdentifier:@
"attachment"
URL:pathUrl options:nil error:&err];
if
(resource_attachment) {
self.bestAttemptContent.attachments = @[resource_attachment];
}
if
(error) {
NSLog(@
"%@"
, error);
}
//设置为@""以后,进入app将没有启动页
self.bestAttemptContent.launchImageName = @
""
;
UNNotificationSound *sound = [UNNotificationSound defaultSound];
self.bestAttemptContent.sound = sound;
//回调给系统
self.contentHandler(self.bestAttemptContent);
}
else
{
self.contentHandler(self.bestAttemptContent);
}
}];
[task resume];
}
else
{
self.contentHandler(self.bestAttemptContent);
}
}
6
- (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);
}
|
WWDC2016上的俄罗斯口音小伙上台讲Notification Service Extension的时候,明确提到了”You will get a short execution time, which means this is not for long background running tasks.“,但实际测试过程中,Notification Service Extension非常容易崩溃crash和内存溢出out of memory。
更加坑的是debug运行的时候和真机运行的时候,Notification Service Extension性能表现是不一样的,真机运行的时候Notification Service Extension非常容易不起作用,我做了几次实验,图片稍大,Notification Service Extension就崩溃了不起作用了,而相同的debug调试环境下则没问题,我觉得他应该也提提这个,比如说你下载资源的时候最好分段缓存下载,真机环境下NSURLSessionDataTask下载数据不好使,必须使用NSURLSessionDownloadTask才可以,这点很无奈。
iOS10 Notification Content Extension:
自定义通知UI
Notification Content Extension是另外一个扩展,其内容使用了UserNotificationsUIFramework,首先还是创建Notification Content Extension的target。
Notification Content Extension
此时会得到Notification Content Extension与MainInterface,storyboard里面含有一个试图控制器,这个试图控制器就是Notification点击后中间显示的那部分。这部分你可以自定义UI,注意的是该视图控制器无法响应交互控件,要想使用交互组件,就必须配合UNNotificationAction和category来对应你的UI部分,还有一点,Notification Content Extension只能有一个控制器,所以你要想定制多种UI,就需要代码判断加载不同的View来实现。
自定义UI部分.png
在视图控制器部分,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
- (void)didReceiveNotification:(UNNotification *)notification {
self.label.text = notification.request.content.body;
UNNotificationAttachment * attachment = notification.request.content.attachments.firstObject;
if
(attachment) {
//开始访问pushStore的存储权限
[attachment.URL startAccessingSecurityScopedResource];
NSData * data = [NSData dataWithContentsOfFile:attachment.URL.path];
[attachment.URL stopAccessingSecurityScopedResource];
self.imageView.image = [UIImage imageWithData:data];
}
}
- (void)didReceiveNotificationResponse:(UNNotificationResponse *)response completionHandler:(void (^)(UNNotificationContentExtensionResponseOption option))completion;{
if
([response isKindOfClass:[UNTextInputNotificationAction class]]) {
//处理提交文本的逻辑
}
if
([response.actionIdentifier isEqualToString:@
"see1"
]) {
//处理按钮3
}
if
([response.actionIdentifier isEqualToString:@
"see2"
]) {
//处理按钮2
}
//可根据action的逻辑回调的时候传入不同的UNNotificationContentExtensionResponseOption
completion(UNNotificationContentExtensionResponseOptionDismiss);
}
|
加入你有了Service Extension在前面下载好了图片或者是视频,在自定义UI部分你想获取,就可以通过UNNotificationAttachment * attachment = notification.request.content.attachments.firstObject;查找附件来获取数据,但是必须注意,前面提到的是,形成附件后,文件的实际存储被移到了pushStore的一个系统级别的缓存文件夹,此时需要调用NSURL在iOS8开始提供的两个方法来获取权限,提取数据。
startAccessingSecurityScopedResource
stopAccessingSecurityScopedResource
点击按钮后,回调的方法是didReceiveNotificationResponse,在前面已经演示过,在这,可以不用打开App进而完成一些交互动作。
Info.plist文件有一些设置需要表明
Info.plist设置
UNNotificationExtensionCategory改成数组,将你自定义的UI支持的categoryIdentifier一一放上,这样,APNs推过来的数据中category包含哪个值,就调用哪个UNNotificationCategory设置好的actions交互组合
UNNotificationExtensionInitialContentSizeRatio,自定义内容的高度与宽度的比值,当然也可以在ViewDidLoad中修改preferredContentSize来完成这一目标
UNNotificationExtensionDefaultContentHidden,决定是否在自定义UI下部显示通知的原内容,默认是显示
Extensions 数据共享:
ServiceExtension与ContentExtension配合使用是非常棒的组合,在ServiceExtension中预先下载好数据,用户点击后在ContentExtension中直接展示,这样交互会比较流畅,有一个问题是,如果你想在不打开App的时候使用自定义的action来与用户交互,就必须加ContentExtension,因为只有它能接收用户点击action的response,ServiceExtension是没有的。
如果你想使你的App在打开的时候访问到这些数据,同样可以根据UNNotificationAttachment来查找,但是更好的方案我个人觉得可以是用App Group来解决这个问题,当然App Group的过多讨论是偏离本文章的话题的。
动态配置通知交互:
上面我们可以知道,Notification可以配上很多category与action来自定义交互方式,但都是硬编码来实现,有时候我们想让某个actionIdentifier对应的按钮文字改变一下,或者是某个category对应的actions改变一下,来满足运营活动的灵活性,需要思考动态配置UNNotificationCategory和UNNotificationAction的问题。有如下这个方案,可以把UNNotificationCategory和UNNotificationAction做成配置文件,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
{
"NotificationConfig"
: {
"UNNotificationCategory"
: {
"seeCategory"
: {
"identifier"
:
"seeCategory"
,
"actions"
: [
"see1"
,
"see2"
],
"options"
: 0
}
},
"UNNotificationAction"
: {
"see1"
: {
"identifier"
:
"see1"
,
"title"
:
"I love it~"
,
"options"
: 4
},
"see2"
: {
"identifier"
:
"see2"
,
"title"
:
"I dont't care~"
,
"options"
: 4
}
}
}
}
|
然后在通知中心设置categorys的时候
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
29
30
31
32
33
|
-(NSSet *)createNotificationCategoryActions{
if
(HBCONFIGOBJECT.moduleConfig.userNotificationConfig) {
//读取json文件
NSDictionary * notificationConfig;
NSDictionary * UNNotificationCategorys =[notificationConfig objectForKey:@
"UNNotificationCategory"
];
NSDictionary * UNNotificationActions = [notificationConfig objectForKey:@
"UNNotificationAction"
];
NSMutableSet * set = [NSMutableSet set];
for
(NSString * categoryKey
in
UNNotificationCategorys.allKeys) {
NSDictionary * cateDict = UNNotificationCategorys[categoryKey];
NSString * cateId = [cateDict objectForKey:@
"identifier"
];
NSArray * cateActions = [cateDict objectForKey:@
"actions"
];
NSNumber * cateOptions = [cateDict objectForKey:@
"options"
];
NSMutableArray * actionsArr = [[NSMutableArray alloc]init];
for
(NSString * actionKey
in
cateActions) {
NSDictionary * actionDict = [UNNotificationActions objectForKey:actionKey];
if
(actionDict) {
NSString * actionId = [actionDict objectForKey:@
"identifier"
];
NSString * actionTitle = [actionDict objectForKey:@
"title"
];
NSNumber * actionOption = [actionDict objectForKey:@
"options"
];
UNNotificationAction * action = [UNNotificationAction actionWithIdentifier:actionId title:actionTitle options:actionOption.unsignedIntegerValue];
[actionsArr addObject:action];
}
}
UNNotificationCategory * category = [UNNotificationCategory categoryWithIdentifier:cateId actions:actionsArr intentIdentifiers:cateActions options:cateOptions.unsignedIntegerValue];
[set addObject:category];
}
return
set;
}
else
{
return
nil;
}
}
|
这样就可以任意组合category和actions了,json文件可以在App内部做全量更新,在运营活动之前,就下发好给客户端。
有一个问题是,ContentExtension需要在plist里指定category,所以建议将categoryIdentifier按照一定格式进行序列化取名,在ContentExtension提前写入0-10等很多的category,方面动态配置的时候取用。
运营如何使用通知与推送:
Instagram上iOS10通知的使用
iOS10推出了十分出色的通知以后,我经常使用的Instagram、Twitter、Facebook等都及时跟进,做出了非常好的交互,我希望微信团队能在通知上快速预览内容和回复上面增加此功能。
其实,这个话题是我非常想讨论的,作为工程师,有得天独厚的条件深刻理解最新最前沿的技术,那么,这些技术如何产生现实意义,如何使用,在这点上,工程师是非常具有优势的,假如你了解硅谷的工程师文化,你就会发现,硅谷的科技公司很少有产品经理的,大部分出色的功能和优质的用户体验是由工程师打造的,详情可以参考MacTalk的一篇文章《硅谷不需要产品经理》。
真正的工程师文化,不像国内的开发者认为的是在某一技术领域非常深的理论研究,在国外的开发者眼里,真正的工程师文化是一群善于创造并且有巨大的改变现实世界的能力的工程师文化,话说回来,现如今,移动端的工程师很多很多,像本篇这样的技术介绍类的文章数不胜数,技术水平差不多的工程师非常之多,你如何脱颖而出?这是你需要思考的,我的建议是,作为工程师,跟你一样熟悉API和开发技术的人多了去了,但是如果你能知道技术在各种场景下的最佳使用方案,并且能切实改变现实情况,举个,iOS10的通知你是了解,但怎么用才能更好的提升你的App的用户体验?更好的提高你的App在某些功能场景下的用户使用成本?怎么样才能让运营活动通过通知提高活跃度?如果你有这样的各种解决方案,你就是胜出者~比方说,你是社交类App的开发者,你有一堆技术解决方案在手,能够切实提升用户体验的,你是电商类开发者,你有通知的技术使用解决方案能够更好的支撑运营活动的。
那iOS10的通知能想到哪些使用场景呢?
运营活动可以配上活动海报或者是动图海报,在用户点击好能更好的查看运营活动详情
即时通讯类的App可以通过自定义ContentExtension来在通知上完成回复消息
比方说,你有个秒杀活动,通知一下来,用户立马可以通过iOS10的通知交互完成秒杀预定,然后再启动App慢慢付款~这个用户体验的提升那是相当巨大的
比方说,你可以通过推送收取一些用户对某个活动或者新版本的反馈意见?使用TextAction来做
你是否可可以发个可视化的账单给用户,在自定义UI上显示?
……
再来说说技术方案吧,上面的场景要想实现,有个问题是,通知的ServiceExtension和ContentExtension拿到了用户反馈的信息,那这些信息该怎么办~方案如下:
最新通知交互方案
这是个简单的单推交互方案,其中需要由动态化配置Category与actions支撑,同时要做好加解密工作。
以上,就是本次讨论通知和推送的主要内容
番外:推拉结合与Web Service Push
//番外篇待补全