参考文章:
本地推送:一、iOS推送之本地推送(iOS Notification Of Local Notification)
二、iOS中使用本地通知为你的APP添加提示用户功能
远程推送:一、iOS推送之远程推送(iOS Notification Of Remote Notification)
二、iOS本地推送与远程推送详解
注:此文现在已经不能适配iOS10了,iOS10的推送采用了新的方法,做iOS9及以下的系统可读此篇文章。
iOS10的推送方法可以查看:iOS10-推送(本地和远程)的简单使用
tips:1.如果想要将通知栏上方的通知清除,只要将APP的角标置为0就可以了。
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];
IOS中消息的推送有两种方式,分别是本地推送和远程推送(APNS)。
一、本地推送
本地推送介绍
本地推送和远程推送的功能是一样的,都是要提醒用户去做某些事情。但是和远程推送不同的就是本地推送是不需要设备联网的,而远程推送是必需要设备联网的,因为只有联网状态下,才能和苹果的APNs服务器建立长连接,从而推送消息。本地推送是由App自己设定的,并且发送给安装此App的这台设备,属于一对一的对应关系。
首先,我们先要明白一个概念,这里的本地推送是UILocalNotification类,和系统的NSNotificationCenter通知中心是完全不同的概念。
本地推送,实际上是由IOS系统管理的一个功能,比如某些后台应用做了某项活动需要我们处理、已经退出的应用在某个时间提醒我们唤起等等,如果注册了通知,系统都会在通知触发时给我们发送消息。由此,我们可以通过系统给我们的APP添加通知用户的功能,并且应用非常广泛。例如,闹种类应用,有按时签到相似功能的应用。
注意:一个App最多只能设置64个本地推送,当超过此限制的时候,系统会自动忽略多余的本地推送,而保留能最快触发的64个。循环的本地推送会被系统认为是同一个本地推送。
创建本地推送,需要用到UILocalNotification类
先来看它的几个属性:
@property(nonatomic,copy) NSDate *fireDate; 设置系统发送通知的时间(如果是过去的时间或者0,则会立刻发起通知)
@property(nonatomic,copy) NSTimeZone *timeZone;// 设置时间的时区
@property(nonatomic) NSCalendarUnit repeatInterval;// 设置周期性通知
NSCalendarUnit对象是枚举,设定通知的周期
typedef NS_OPTIONS(NSUInteger, NSCalendarUnit) {
NSCalendarUnitEra = kCFCalendarUnitEra,
NSCalendarUnitYear = kCFCalendarUnitYear,
NSCalendarUnitMonth = kCFCalendarUnitMonth,
NSCalendarUnitDay = kCFCalendarUnitDay,
NSCalendarUnitHour = kCFCalendarUnitHour,
NSCalendarUnitMinute = kCFCalendarUnitMinute,
NSCalendarUnitSecond = kCFCalendarUnitSecond,
NSCalendarUnitWeekday = kCFCalendarUnitWeekday,
NSCalendarUnitWeekdayOrdinal = kCFCalendarUnitWeekdayOrdinal,
}
@property(nonatomic,copy) NSCalendar *repeatCalendar;// 设置周期性通知参照的日历表,NSCalendar的使用方法请自行查找
@property(nonatomic,copy) CLRegion *region;// 在用户进去或者离开某一区域时发送通知
@property(nonatomic,assign) BOOL regionTriggersOnce; 设置区域检测通知是否重复(如果为YES,则没次进入该区域都会发送,否则只发送一次)
@property(nonatomic,copy) NSString *alertBody; // 设置通知的主体内容
@property(nonatomic) BOOL hasAction; //是否隐藏滑动启动按钮
@property(nonatomic,copy) NSString *alertAction;// 设置滑动打开的提示文字
@property(nonatomic,copy) NSString *alertLaunchImage; // 设置点击通知后启动的启动图片
@property(nonatomic,copy) NSString *alertTitle;//通知的短标题
@property(nonatomic,copy) NSString *soundName; // 收到通知时,播放的系统音
@property(nonatomic) NSInteger applicationIconBadgeNumber; //设置应用程序Icon头标数字
@property(nonatomic,copy) NSDictionary *userInfo; // 用户字典,可用于传递通知消息参数
NSString *const UILocalNotificationDefaultSoundName; // 推送提示音,注意:这个字符串是系统默认的提示音
创建一个本地推送通知
// 在需要添加本地通知的地方进行添加处理
// 1.创建一个本地通知
UILocalNotification *localNote = [[UILocalNotification alloc] init];
// 1.1.设置通知发出的时间
localNote.fireDate = [NSDate dateWithTimeIntervalSinceNow:100];
//设置重复间隔
localNote.repeatInterval = NSCalendarUnitMinute;
// 1.2.设置通知内容
localNote.alertBody = @"这是一个推送这是一个推送";
// 1.3.设置锁屏时,字体下方显示的一个文字
// localNote.alertAction = @"赶紧!!!!!";
// 1.4.设置启动图片(通过通知打开的)
localNote.alertLaunchImage = @"144";
// 1.5.设置通过到来的声音
localNote.soundName = UILocalNotificationDefaultSoundName;
// 1.6.设置应用图标左上角显示的数字
localNote.applicationIconBadgeNumber = 999;
// 1.7.设置一些额外的信息
localNote.userInfo = @{@"qq" : @"1234567", @"msg" : @"success"};
// 2 设置好本地推送后必须调用此方法启动此推送
[[UIApplication sharedApplication] scheduleLocalNotification:localNote];
现在,一个简单的本地推送已经创建好了,如果需要在推送执行完毕后,取消推送,可以使用下面的方法
// 1、取消某一个通知
NSArray *notificaitons = [[UIApplication sharedApplication] scheduledLocalNotifications];
//获取当前所有的本地通知
if (!notificaitons || notificaitons.count <= 0) {
return;
}
for (UILocalNotification *notify in notificaitons) {
if ([[notify.userInfo objectForKey:@"id"] isEqualToString:@"想要取消的推送的标识符"]) {
//取消一个特定的通知
[[UIApplication sharedApplication] cancelLocalNotification:notify];
break;
}
}
// 2、取消所有的本地通知
[[UIApplication sharedApplication] cancelAllLocalNotifications];
本地推送的注册与处理
分为本地推送和远程推送2种。可以在应用没有打开甚至手机锁屏情况下给用户以提示。它们都需要注册,注册后系统会弹出提示框(如下图)提示用户是否同意,如果同意则正常使用;如果用户不同意则下次打开程序也不会弹出该提示框,需要用户到设置里面设置。一共有三种提示类型:
UIUserNotificationTypeBadge:应用图标右上角的信息提示
UIUserNotificationTypeSound:播放提示音
UIUserNotificationTypeAlert:提示框
APP安装后,第一次打开时,会弹出提示框询问用户是否允许推送,用户选择之后,以后都不会再进行提示,除非用户卸载重装:
想要正常进行推送,就必须要进行注册:
/// 一般在程序启动时注册通知
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
if (10.0>[[UIDevice currentDevice].systemVersion floatValue] >= 8.0) { // iOS8
// categories 可以设置不同类别的推送,在收到推送的代理方法中根据categories的identifier进行不同的推送处理
UIUserNotificationSettings *setting = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeBadge | UIUserNotificationTypeAlert | UIUserNotificationTypeSound categories:nil];
[application registerUserNotificationSettings:setting];
}
// 如果用户对程序进行滑飞操作,程序进程被杀死后,重新启动APP,推送的代理方法是不走的,这个时候就可以添加下面的代码,获取到推送,进行对应的操作。
NSDictionary *userInfoLocal = launchOptions[UIApplicationLaunchOptionsLocalNotificationKey];
if (userInfoLocal) {
// 这里添加处理代码
NSLog(@"=== Local:%@", userInfoLocal);
}
return YES;
}
接下来就需要对收到的推送信息进行处理了
/** 获取用户对通知的设置信息 */ //NS_AVAILABLE_IOS(8_0);
- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings {
}
// 程序处于前台(或者程序处于后台,但是用户点击了收到的通知信息),就会运行该方法 //NS_AVAILABLE_IOS(4_0, 10_0);
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {
/* notification === {fire date = 2016年12月19日 星期一 中国标准时间 上午11:31:27, time zone = (null), repeat interval = NSCalendarUnitMinute, repeat count = UILocalNotificationInfiniteRepeatCount, next fire date = 2016年12月19日 星期一 中国标准时间 上午11:32:27, user info = {
msg = success;
qq = 1234567;
}}*/
// 这里添加处理代码
}
如果在上面注册通知的时候,携带了 categories 参数,则会调用下面的方法。
//NS_AVAILABLE_IOS(8_0, 10_0);
- (void)application:(UIApplication *)application handleActionWithIdentifier:(nullable NSString *)identifier forLocalNotification:(UILocalNotification *)notification completionHandler:(void(^)())completionHandler {
// identifier是识别符,
//处理完消息,最后一定要调用这个代码块
completionHandler();
}
// 下面这个方法是 9.0新加的,如果要是适配8.0,只需要用上面的方法就可以了,参数 responseInfo 可以通过 notification.userInfo 取到。
//NS_AVAILABLE_IOS(9_0, 10_0);
- (void)application:(UIApplication *)application handleActionWithIdentifier:(nullable NSString *)identifier forLocalNotification:(UILocalNotification *)notification withResponseInfo:(NSDictionary *)responseInfo completionHandler:(void(^)())completionHandler {
}
以上就是关于 iOS8.0-10.0 的本地推送的使用了。
注:上面提到的有关 categories 参数的使用,我没有进行测试,如果使用时有问题,欢迎给我留言。
二、远程推送
想要使用远程推送,需要先在开发者网站申请推送证书
远程推送原理
注:(从作者TIME_for的文章复制过来的)
学习一些东西前我认为最好能了解它的原理,这样以后我们遇到问题的时候,就可以很快速的找到错误之所在,如果对原理不感兴趣的同学可直接下翻到应用部分。
iOS app大多数都是基于client/server模式开发的,client就是安装在我们设备上的app,server就是远程服务器,主要给我们的app提供数据,因为也被称为Provider。那么问题来了,当App处于Terminate状态的时候,当client与server断开的时候,client如何与server进行通信呢?是的,这时候Remote Notifications很好的解决了这个困境。苹果所提供的一套服务称之为Apple Push Notification service,就是我们所谓的APNs。
推送消息传输路径: Provider-APNs-Client App
我们的设备联网时(无论是蜂窝联网还是Wi-Fi联网)都会与苹果的APNs服务器建立一个长连接(persistent IP connection),当Provider推送一条通知的时候,这条通知并不是直接推送给了我们的设备,而是先推送到苹果的APNs服务器上面,而苹果的APNs服务器再通过与设备建立的长连接进而把通知推送到我们的设备上(参考图1-1,图1-2)。而当设备处于非联网状态的时候,APNs服务器会保留Provider所推送的最后一条通知,当设备转换为连网状态时,APNs则把其保留的最后一条通知推送给我们的设备;如果设备长时间处于非联网状态下,那么APNs服务器为其保存的最后一条通知也会丢失。Remote Notification必须要求设备连网状态下才能收到,并且太频繁的接收远程推送通知对设备的电池寿命是有一定的影响的。
deviceToken的生成
当一个App注册接收远程通知时,系统会发送请求到APNs服务器,APNs服务器收到此请求会根据请求所带的key值生成一个独一无二的value值也就是所谓的deviceToken,而后APNs服务器会把此deviceToken包装成一个NSData对象发送到对应请求的App上。然后App把此deviceToken发送给我们自己的服务器,就是所谓的Provider。Provider收到deviceToken以后进行储存等相关处理,以后Provider给我们的设备推送通知的时候,必须包含此deviceToken。
这个时候你可能会问deviceToken到底是什么?有什么用?为什么是独一无二的?
是什么:deviceToken其实就是根据注册远程通知的时候向APNs服务器发送的Token key,Token key中包含了设备的UDID和App的Bundle Identifier,然后苹果APNs服务器根据此Token key编码生成一个deviceToken。deviceToken可以简单理解为就是包含了设备信息和应用信息的一串编码。
有什么用:上面提到Provider推送消息的时候必须带有此deviceToken,然后此消息就根据deviceToken(UDID + App's Bundle Identifier)找到对应的设备以及该设备上对应的应用,从而把此推送消息推送给此应用。
唯一性:苹果APNs的编码技术和deviceToken的独特作用保证了他的唯一性。每台设备的 deviceToken都是不一样的。
注:deviceToken也是会变的,将用户将程序卸载重装或者用户升级系统的时候deviceToken是会变化的。 ”If the user restores backup data to a new device or computer, or reinstalls the operating system, the device token changes“,因此应每次都发给服务器(provider)
远程推送的注册与处理
远程推送的注册
远程推送的注册与本地推送相比,只需要加一行代码就可以了。
/// 一般在程序启动时注册通知
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
if (10.0>[[UIDevice currentDevice].systemVersion floatValue] >= 8.0) { // iOS8
// categories 可以设置不同类别的推送,在收到推送的代理方法中根据categories的identifier进行不同的推送处理
UIUserNotificationSettings *setting = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeBadge | UIUserNotificationTypeAlert | UIUserNotificationTypeSound categories:nil];
[application registerUserNotificationSettings:setting];
}
/**注册远程通知要添加的代码,该代码可以写在这里,也可以写在获取用户设置推送信息的回调方法里,didRegisterUserNotificationSettings*/
[[UIApplication sharedApplication] registerForRemoteNotifications];
/*
// iOS7及其之后,无论是程序被杀死还是处于后台,只要用户点击了通知,都会调用“- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler ”方法,因此如果是iOS7及以上系统,则不必在didFinishLaunchingWithOptions中做处理,只在该方法中做处理即可,此时应避免在didFinishLaunchingWithOptions函数中也做重复处理。
NSDictionary *userInfoLocal = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey];
if (userInfoLocal) {
// 这里添加处理代码
NSLog(@"=== Local:%@", userInfoLocal);
}
*/
return YES;
}
远程推送的代理方法
/** 获取用户对通知的设置信息 */ //NS_AVAILABLE_IOS(8_0);
- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings {
}
/** 远程通知注册成功委托 */ //NS_AVAILABLE_IOS(3_0);
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
// 对 deviceToken 进行处理
NSString *token = [[deviceToken description] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]];
token = [token stringByReplacingOccurrencesOfString:@" " withString:@""];
// 需要把 deviceToken 上传给服务器
/**上传deviceToken的代码*/
}
/** 远程通知注册失败委托 */ //NS_AVAILABLE_IOS(3_0);
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
NSLog(@"%@",error);
}
收到远程推送的回调方法
// 收到远程推送时,程序在前台,或者程序在后台但是点击了收到的推送通知打开程序 //NS_AVAILABLE_IOS(7_0);
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler{
// 在此方法中一定要调用completionHandler这个回调,告诉系统是否处理成功
// UIBackgroundFetchResultNewData, 成功接收到数据
// UIBackgroundFetchResultNoData, 没有接收到数据
// UIBackgroundFetchResultFailed 接受失败
if (userInfo) {
completionHandler(UIBackgroundFetchResultNewData);
} else {
completionHandler(UIBackgroundFetchResultNoData);
}
}
//NS_AVAILABLE_IOS(3_0, 10_0); // 如果是iOS8.0以上的系统,不会运行该方法,只会运行上面的方法
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
}
如果在上面注册通知的时候,携带了 categories 参数,则会调用下面的方法。
//NS_AVAILABLE_IOS(8_0, 10_0);
- (void)application:(UIApplication *)application handleActionWithIdentifier:(nullable NSString *)identifier forRemoteNotification:(NSDictionary *)userInfo completionHandler:(void(^)())completionHandler {
//处理完消息,最后一定要调用这个代码块
completionHandler();
}
// 下面这个方法是 9.0新加的,如果要是适配8.0,只需要用上面的方法就可以了,参数 responseInfo 可以通过 notification.userInfo 取到。
//NS_AVAILABLE_IOS(9_0, 10_0);
- (void)application:(UIApplication *)application handleActionWithIdentifier:(nullable NSString *)identifier forRemoteNotification:(NSDictionary *)userInfo withResponseInfo:(NSDictionary *)responseInfo completionHandler:(void(^)())completionHandler {
}
设置程序后台运行
设置后台运行后,收到远程推送后会调用方法:
application: didReceiveRemoteNotification: fetchCompletionHandler:
iOS7之前苹果是不支持多任务的,这也是iOS系统对硬件要求低,流畅性好的原因之一。iOS7之后,苹果开始支持多任务,即App可在后台做一些更新UI、下载数据的操作等。若要接收到远程推送的时候要在后台做一些事情则需要把后台远程推送模式打开。不适配iOS7之前系统的项目建议使用此后台模式,充分利用苹果推出的多任务模式,不枉费苹果的一片苦心啊!设置后台模式方法项目对应TARGETS-Capabilities-Background Modes-Remote Notifications具体设置方法如下图。
远程推送负载
远程推送负载大小
远程通知负载的大小根据Provider使用的API不同而不同。当使用HTTP/2 provider API时,负载最大为4096bytes,即4kB;当使用legacy binary interface时,负载最大为2048bytes,即2kB。当负载大小超过规定的负载大小时,APNs会拒绝发送此消息。
远程推送负载内容格式
一般公司服务端会要我们客户端定义好格式给他们。
每一条通知的消息都会组成一个JSON字典对象,其格式如下所示,示例中的key值为苹果官方所用key。自定义字段的时候要避开这些key值。
{ "aps" : {
"alert" : { // string or dictionary
"title" : "string"
"body" : "string",
"title-loc-key" : "string or null"
"title-loc-args" : "array of strings or null"
"action-loc-key" : "string or null"
"loc-key" : "string"
"loc-args" : "array of strings"
"launch-image" : "string"
},
"badge" : number,
"sound" : "string"
"content-available" : number;
"category" : "string"
},
}
aps:推送消息必须有的keyalert:推送消息包含此key值,系统就会根据用户的设置展示标准的推送信息
badge:在app图标上显示消息数量,缺少此key值,消息数量就不会改变,消除标记时把此key对应的value设置为0
sound:设置推送声音的key值,系统默认提示声音对应的value值为default
content-available:此key值设置为1,系统接收到推送消息时就会调用不同的回调方法,iOS7之后配置后台模式
category:UIMutableUserNotificationCategory's identifier 可操作通知类型的key值
title:简短描述此调推送消息的目的,适用系统iOS8.2之后版本body:推送的内容
title-loc-key:功能类似title,附加功能是国际化,适用系统iOS8.2之后版本
title-loc-args:配合title-loc-key字段使用,适用系统iOS8.2之后版本
action-loc-key:可操作通知类型key值,不详细叙述
loc-key:参考title-loc-key
loc-args:参考title-loc-args
launch-image:点击推送消息或者移动事件滑块时,显示的图片。如果缺少此key值,会加载app默认的启动图片。
当然以上key值并不是每条推送消息都必带的key值,应当根据需求来选择所需要的key值,除了以上系统所提供的key值外,你还可以自定义自己的key值,来作为消息推送的负载,自定义key值与aps此key值并列。如下格式:
{ "aps" : {
"alert" : "Provider push messag.",
"badge" : 9,
"sound" : "toAlice.aiff"
},
"Id" : 1314, // 自定义key值
"type" : "customType" // 自定义key值
}
推送函数的调用:
首次安装后启动:
didRegisterForRemoteNotificationsWithDeviceToken 被调用
系统询问用户是否同意接收 Notifications
不管用户选择同意或拒绝,didRegisterUserNotificationSettings 被调用
应用非首次启动时:
如果 notifications 处于拒绝状态:didRegisterUserNotificationSettings 被调用
如果 notifications 处于允许状态
didRegisterForRemoteNotificationsWithDeviceToken 被调用
didRegisterUserNotificationSettings 被调用
应用运行过程中用户修改 notifications 设置:
从拒绝变为允许:**didRegisterForRemoteNotificationsWithDeviceToken **被调用
从允许变为拒绝:什么也不发生。