项目之前一直用的Umeng推送,之前的本地推送也只是略略的设置过并不了解。在iOS 10之后,苹果直接将推送封装成了具体的框架后,借着这个机会,将系统的通知文档翻译了一遍,整理了一些自己的关注点和不理解的地方:
1 官方文档解读
1.1 推送预览
2 推送状态判断
3 本地推送
3 远程推送
4 资料推荐
1 官方文档解读
推送就是即便应用没有运行在前台,也能在你的应用获得新数据时通过banner、alert和badge等方式通知用户的工具。在iOS 10的时候,苹果将推送封装成UserNotifications和UserNotificationsUI两个框架,并且在对推送所使用的关键类以及之间的关系定义的非常明确。
1.1 本地推送
本地推送就是相关的代码写在本地,并会以一种合适的方式触发来提示用户。本地推送是不需要开启 Push Notifications功能和请求证书的,但是显示的界面和远程推送没什么两样。对于本地推送实现的逻辑思路路线如下:
步骤一 UNMutableNotificationContent 推送内容
@interface UNMutableNotificationContent : UNNotificationContent
// 可选的附件数组
@property (NS_NONATOMIC_IOSONLY, copy) NSArray <UNNotificationAttachment *> *attachments;
// 应用icon的标记数。nil标识没有改变,0表示隐藏
@property (NS_NONATOMIC_IOSONLY, copy, nullable) NSNumber *badge;
// 通知的主体,使用-[NSString localizedUserNotificationStringForKey:arguments:]方法设置的字符串能在显示的时候被本地化
@property (NS_NONATOMIC_IOSONLY, copy) NSString *body;
// 一个注册的UNNotificationCategory标识符将用来决定合适的行为
@property (NS_NONATOMIC_IOSONLY, copy) NSString *categoryIdentifier;
// 当应用从推送打开时使用的启动图
@property (NS_NONATOMIC_IOSONLY, copy) NSString *launchImageName;
// 推送播放的声音
@property (NS_NONATOMIC_IOSONLY, copy, nullable) UNNotificationSound *sound;
// 通知的子标题,使用 -[NSString localizedUserNotificationStringForKey:arguments:]本地化字符串
@property (NS_NONATOMIC_IOSONLY, copy) NSString *subtitle;
// 推送标题,使用-[NSString localizedUserNotificationStringForKey:arguments:]本地化字符串
@property (NS_NONATOMIC_IOSONLY, copy) NSString *title;
// 远程推送的内容
@property (NS_NONATOMIC_IOSONLY, copy) NSDictionary *userInfo;
其中:
①UNNotificationSound播放的声音要求在30秒内,超时或者没有找到相关的文件会播放默认的“叮”的那个声音。而且声音播放是系统原生播放器,需要满足系统播放的条件(符合Linear PCM、MA4 (IMA/ADPCM)、 μLaw、aLaw的音频格式)。
②UNNotificationAttachment表示推送携带的附件,可以是图片、声音和视频,但是如果文件路径为nil,导致携带的附件为nil,并且将nil的附件赋值给content的attachments值是会直接崩溃报错。
步骤二 UNTimeIntervalNotificationTrigger 触发条件
苹果提供三种触发条件 :
UNTimeIntervalNotificationTrigger(几秒钟之后触发)
// 用在指明在几秒钟之后触发,可选是否重复
@interface UNTimeIntervalNotificationTrigger : UNNotificationTrigger
@property (NS_NONATOMIC_IOSONLY, readonly) NSTimeInterval timeInterval;
+ (instancetype)triggerWithTimeInterval:(NSTimeInterval)timeInterval repeats:(BOOL)repeats;
- (nullable NSDate *)nextTriggerDate;
@end
UNCalendarNotificationTrigger(某个日期触发)
// 基于日期和时间,可选是否重复。
@interface UNCalendarNotificationTrigger : UNNotificationTrigger
@property (NS_NONATOMIC_IOSONLY, readonly, copy) NSDateComponents *dateComponents;
+ (instancetype)triggerWithDateMatchingComponents:(NSDateComponents *)dateComponents repeats:(BOOL)repeats;
- (nullable NSDate *)nextTriggerDate;
@end
UNLocationNotificationTrigger(某个地区触发)
// 当用户进入或者离开一个地理区域时,CLRegion的标识符必须是唯一的。可选是否重复。
@interface UNLocationNotificationTrigger : UNNotificationTrigger
@property (NS_NONATOMIC_IOSONLY, readonly, copy) CLRegion *region;
+ (instancetype)triggerWithRegion:(CLRegion *)region repeats:(BOOL)repeats __WATCHOS_PROHIBITED;
@end
步骤三 UNNotificationRequest 推送请求
@interface UNNotificationRequest : NSObject <NSCopying, NSSecureCoding>
//通知请求的唯一标识符,能用来代替或者移除一个即将运行的推送请求。
@property (NS_NONATOMIC_IOSONLY, readonly, copy) NSString *identifier;
// 通知的内容
@property (NS_NONATOMIC_IOSONLY, readonly, copy) UNNotificationContent *content;
// 如果没有triger触发条件,意味了马上就推送给用户
@property (NS_NONATOMIC_IOSONLY, readonly, copy, nullable) UNNotificationTrigger *trigger;
+ (instancetype)requestWithIdentifier:(NSString *)identifier content:(UNNotificationContent *)content trigger:(nullable UNNotificationTrigger *)trigger;
- (instancetype)init;
@end
####### 步骤四 UNUserNotificationCenter 将请求发送给APNs
- (void)addNotificationRequest:(UNNotificationRequest *)request withCompletionHandler:(nullable void(^)(NSError *__nullable error))completionHandler;
步骤五 UNTextInputNotificationAction 行为(可选)
用户对该推送所能进行的自定义行为,如:
UNNotificationAction *likeYouAction = [UNNotificationAction actionWithIdentifier:@"like" title:@"想看!!" options:UNNotificationActionOptionAuthenticationRequired | UNNotificationActionOptionDestructive | UNNotificationActionOptionForeground];
UNNotificationAction *notLikeAction = [UNNotificationAction actionWithIdentifier:@"notLike" title:@"不想要看啊" options:UNNotificationActionOptionAuthenticationRequired | UNNotificationActionOptionDestructive];
行为的options值为:
typedef NS_OPTIONS(NSUInteger, UNNotificationActionOptions) {
// 在被执行前该行为是否要求解锁(不会进入应用)
UNNotificationActionOptionAuthenticationRequired = (1 << 0),
//是否应该表示为破坏性的(红色字体,也不会进入应用)
UNNotificationActionOptionDestructive = (1 << 1),
//该行为是否应用让应用显示在前台
UNNotificationActionOptionForeground = (1 << 2),
};
并且将同一性质的actions加入一个category中:
UNNotificationCategory *category1 = [UNNotificationCategory categoryWithIdentifier:@"ownerFavorite" actions:@[likeYouAction,notLikeAction] intentIdentifiers:@[@"like",@"notLike"] options:UNNotificationCategoryOptionHiddenPreviewsShowTitle];
2 推送状态判断
在用户第一次打开该应用时,需要向用户请求是否开启推送权限。
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center requestAuthorizationWithOptions:(UNAuthorizationOptionSound + UNAuthorizationOptionBadge + UNAuthorizationOptionCarPlay + UNAuthorizationOptionAlert)
completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (granted == YES) {
NSLog(@"我允许了所有的通知设置");
} else {
NSLog(@"我禁止了所有的通知设置");
}
}];
必须注意的是:granted返回的是下图中 Allow Notifications(是否允许开启推送)的判断值。如果该值打开,下面所有的请求推送方式都关闭了,granted还是会返回YES。
而对于推送方式:badge、alert、sounds等的判断是UNNotificationSettings类来进行判断。
[[UNUserNotificationCenter currentNotificationCenter] getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
/*
typedef NS_ENUM(NSInteger, UNAuthorizationStatus) {
// The user has not yet made a choice regarding whether the application may post user notifications.
用户还没有决定是否应用能推送给用户通知
UNAuthorizationStatusNotDetermined = 0,
// The application is not authorized to post user notifications.
应用没有通过验证推送用户的通知
UNAuthorizationStatusDenied,
// The application is authorized to post user notifications.
应用已经验证通过能推送用户通知
UNAuthorizationStatusAuthorized
} __IOS_AVAILABLE(10.0)
*/
//总体的设置
switch (settings.authorizationStatus) {
case UNAuthorizationStatusDenied:
NSLog(@"用户拒绝了,OMG,为什么");
break;
case UNAuthorizationStatusAuthorized:
NSLog(@"用户已经通过了,哈哈哈哈哈哈");
break;
case UNAuthorizationStatusNotDetermined:
NSLog(@"用户还没有决定");
break;
default:
break;
}
//个别的设置情况
/*
typedef NS_ENUM(NSInteger, UNNotificationSetting) {
// The application does not support this notification type
系统不支持该通知类型
UNNotificationSettingNotSupported = 0,
// The notification setting is turned off.
通知设置关闭了
UNNotificationSettingDisabled,
// The notification setting is turned on.
通知设置开启了
UNNotificationSettingEnabled,
} __IOS_AVAILABLE(10.0) __TVOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0);
*/
NSLog(@"------------------------------对声音的设置------------------------------");
switch (settings.soundSetting) {
case UNNotificationSettingEnabled:
NSLog(@"*************************************已经开启了声音的设置类型");
break;
case UNNotificationSettingDisabled:
NSLog(@"*************************************已经关闭了声音的设置类型");
break;
case UNNotificationSettingNotSupported:
NSLog(@"*************************************不支持了声音的设置类型");
break;
default:
break;
}
NSLog(@"------------------------------对图标标记值的设置------------------------------");
switch (settings.badgeSetting) {
case UNNotificationSettingNotSupported:
NSLog(@"************************************不支持在图标上标记哦");
break;
case UNNotificationSettingDisabled:
NSLog(@"*************************************关闭了当前图标上标记的功能");
break;
case UNNotificationSettingEnabled:
NSLog(@"************************************开启了当前图标上标记的功能");
break;
default:
break;
}
}];
3 本地推送
如果想在早上8:30响起闹钟
//定义了可执行的行为
UNNotificationAction *likeYouAction = [UNNotificationAction actionWithIdentifier:@"like" title:@"欢喜的起床了!!" options:UNNotificationActionOptionAuthenticationRequired | UNNotificationActionOptionDestructive | UNNotificationActionOptionForeground];
UNNotificationAction *notLikeAction = [UNNotificationAction actionWithIdentifier:@"notLike" title:@"走开,我要继续睡" options:UNNotificationActionOptionAuthenticationRequired | UNNotificationActionOptionDestructive];
UNNotificationCategory *category1 = [UNNotificationCategory categoryWithIdentifier:@"ownerFavorite" actions:@[likeYouAction,notLikeAction] intentIdentifiers:@[@"like",@"notLike"] options:UNNotificationCategoryOptionHiddenPreviewsShowTitle];
//自定义了推送的内容
NSString *imagePath = [[NSBundle mainBundle]pathForResource:@"image1" ofType:@"png"];
UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:@"imageGif" URL:url options:@{UNNotificationAttachmentOptionsThumbnailClippingRectKey:[NSValue valueWithCGRect:CGRectMake(0, 0, 1, 1)]} error:nil];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = @"已经早上8:30了,该起床了~~~";
content.subtitle = @"超人该起床抓坏蛋了";
content.body = @"让我们荡起双桨,小船儿~推开~~波浪~~~~~~~";
content.attachments =@[attachment];
content.categoryIdentifier = @"ownerFavorite";
content.badge = @(1);
//定义了推送的触发条件
NSDateComponents *components = [[NSDateComponents alloc] init];
components.hour = 8;
components.minute = 30;
components.second = 0;
UNCalendarNotificationTrigger *calendar = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:NO];
//将通知请求发送给APNs
UNNotificationRequest *notificationRequest = [UNNotificationRequest requestWithIdentifier:@"s" content:content trigger: calendar];
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
center.delegate = self;
[center addNotificationRequest:notificationRequest withCompletionHandler:^(NSError * _Nullable error) {
}];
在该例中,UNNotificationAttachment十分需要注意:
①UNNotificationAttachment的对象创建是由一个文件的URL路径决定的,如果路径找不到,直接导致UNNotificationAttachment创建失败为nil。而如果出现了路径为nil的情况下,需要确保资料的路径添加到当前的target中。
②因为是将UNNotificationAttachment对象加入到content.attachments的数组中,所以一旦UNNotificationAttachment创建失败为nil,将直接导致运行的崩溃。建议在添加到数组前最好加一次判断。
在推送发送到APNs之后,等待推送的触发条件成熟立刻显示在用户界面中,而对于显示,应用有两种状态:
当应用处于后台或者没有运行的状态下,调用方法:
-(void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
当用户点击自定义的行为按钮后,系统会自动运行系统进行相应的处理。
当应用处于前台时,默认出现静默状态的推送,但是能通过设置方法显示通知:
-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
3 远程推送
如果没有服务器,可以使用在线的一些软件进行模拟 ---SmartPush
远程推送的触发判断条件为:UNPushNotificationTrigger。不同于本地推送,远程推送需要和服务器、APNs进行多方的沟通,所以将整个流程思路整理。
步骤一 开启远程通知功能并且请求推送证书
证书的申请就是在苹果开发者平台中进行,网上有详细的说明便不再赘述。
步骤二 将设备令牌发送给服务器
在官方文档中明确的指明:不能将设备令牌缓存在本地或者服务器中,每次都需要从APNs中获取最新的令牌。是因为当设备系统更新或者应用重新下载等情况下,该值是会不断变换的。前提是苹果保证在同一个应用中的设备令牌是唯一的。
-(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
NSString *deviceTokenString = [[[
[deviceToken description]
stringByReplacingOccurrencesOfString:@"<" withString:@""] stringByReplacingOccurrencesOfString:@">" withString:@""] stringByReplacingOccurrencesOfString:@" " withString:@""]
;
NSLog(@"%@",deviceToken);
//全局该token
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:deviceTokenString forKey:@"deviceToken"];
[userDefaults synchronize];
//然后将该token发送给服务器端
}
步骤三 使用UNNotificationServiceExtension 获取远程推送
如果在推送显示给用户前,需要进行一些处理或更换,如下载图片或者视频等,可以设置UNNotificationServiceExtension,那么APNs在发送给用户前,会先发送给UNNotificationServiceExtension进行处理。
如:当服务器的推送内容为:
{
"aps": {
//系统key值设置
"alert": {
"body": "李周推送的信息消息会是什么呢,嘿嘿~~~就不告诉你,就不告诉你",
"title": "震惊!震惊!",
"subtitle": "李周正在推送新的消息中,有新消息了~~~~~"
},
"badge": 6,
"sound": "default",
"category": "ownerFavorite",
"mutable-content":1
},
//服务器端和客户端进行交互设定的key值
"attach": "http://img3.duitang.com/uploads/item/201511/14/20151114232024_B8LZS.thumb.700_0.jpeg"
}
UNNotificationServiceExtension接收到服务器发送的数据后,需要在一个自定义key值中获得图片的地址并进行相应的下载。
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
// Modify the notification content here...
self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title];
//
NSString *urlStr =request.content.userInfo[@"attach"];
NSURL *url = [NSURL URLWithString:urlStr];
self.task = [[NSURLSession sharedSession] downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (!error) {
NSString *path = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:response.suggestedFilename];
[[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:path] error:nil];
UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:@"image" URL:[NSURL fileURLWithPath:path] options:nil error:nil];
self.bestAttemptContent.attachments = @[attachment];
self.contentHandler(self.bestAttemptContent);
}
}];
[self.task resume];
NSString *path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
// self.contentHandler(self.bestAttemptContent);
}
保证对推送内容的处理在30秒内,如果超过时间,系统会直接调用- (void)serviceExtensionTimeWillExpire 方法,显示未下载之前的推送。
所以应该是大多数的应用不考虑图片或视频等远程推送的原因,怕用户会处于弱网的情况下,导致没有将内容完整的推送。
值得注意的是
如果在UNNotificationServiceExtension中开启了Push Notifications的功能,但是无法修改远程通知的内容,原因可能是:
1 运行是开启ServiceExtension的target
2 "mutable-content":1
推送内容中必须设置mutable-content为1,设置为0或者不设置ServiceExtension都无法捕捉到远程推送的内容,也就无法进行修改。
2 设置的Deployment Target
保证所有的target中这个值小于或者等于你设备系统版本。
步骤四 使用UNNotificationContentServiceExtension 自定义显示推送界面
创建一个NotificationContent Extension,其中包括storyboard和ViewController、info.plist文件。
将ViewController当做普通的视图控制器,进行界面的设置。从方法中获得请求中的推送内容:
- (void)didReceiveNotification:(UNNotification *)notification
info.plist文件中有NSExtension键,里面有三个比较重要的设置key值:
UNNotificationExtensionCategory用来识别推送,如果包括该推送的categoryIdentifier,那么使用该自定义的UI提示用户。该值可以是一个字符串或一个数组格式。
UNNotificationExtensionDefaultContentHidden :是否显示系统的文本设置.
设置为YES:表示隐藏系统的文本显示
设置为NO:表示不隐藏系统的文本显示
UNNotificationExtensionIntialContentSizeRatio 设置显示的初始视图的缩放值:
如果自定义的大小不符合你当前设置内容的大小出现以下的问题:
可以设置 self.preferredContentSize = CGSizeMake(self.view.bounds.size.width, 200);来将显示界面固定合适的大小,但是固定之后,会发现通知出现的时候会有一个动画效果:从稍大的初始化界面效果变成大小为200稍小的界面。所以
UNNotificationExtensionIntialContentSizeRatio值的设置就能将初始化界面缩放成之前固定的百分比。
4 资料推荐
其实在我了解推送的整个过程中,第一步把推送的官方文档看了一遍,将一些细节点记录下来;第二步把一些优秀的文章对照着翻译之后的文档看了一遍,并且做了一遍。有很多资料都特别的好,列举一二:
iOS10推送必看UNNotificationServiceExtension(Cocochina)
iOS10推送必看UNNotificationAttachment以及UNTimeIntervalNotificationTrigger(简书)
一周又结束了,推送的功能已经在项目中实现了。下周又能去学一些新的东西了~