iOS中存在三种常见的事件通知方式:NSNofiticationCenter、KVO Notification 和 User Notifications,其中 User Notifications,就是本文将要探讨的用户通知。php
咱们都知道 iOS 系统常常的有一些与 App 相关的通知栏消息,这些消息每每伴随着提示音以及 App 的桌面图标右上角的未读消息提示,这些通知就是 iOS 的用户通知。程序员
用户通知分为两类:本地通知和远程通知,其中远程通知又称为推送通知。json
二者最主要的区别是:本地通知是由 App 发送到当前设备上,不须要网络支持;而远程通知是由 App 的服务器发送到苹果的 APNs 服务器,并由 APNs 服务器转发到相应设备(由 App 服务器指定接收通知的设备)。缓存
二者最主要的共同点是:本地通知和远程通知对用户的表现形式是相同的,二者都可以采用通知栏消息、App 桌面图标右上角角标和提示音的方式通知用户。服务器
及时有效的(不管是在前台仍是后台)向用户发送消息(聊天信息、新闻、待办事项、天气变化等)是用户通知最大的优点。网络
此外,有效合理的使用用户通知,可让咱们的 App 有更好的体验,如:并发
本文后续内容将以应用开发者的角度对用户通知进行深刻的探讨,本文讨论内容针对iOS7/8/9,有关 iOS10 系统的用户通知会另作讲解。app
本文中的远程通知使用了 Simplepush.php ,内部代码很简单,可以使用该脚本自定义远程通知的内容,ide
本文主要参考了苹果官方的 Local and Remote Notification Programming Guide 以及本文用到的接口的官方文档。测试
对于 iOS7,若是用户没有在系统设置里关闭该 App 的通知功能,那么开发者无需作任何操做便可使用本地通知功能。
对于 iOS8 及之后的系统,若须要使用本地通知功能,则须要注册通知类型。
通知类型有四种:角标(UIUserNotificationTypeBadge)、提示音(UIUserNotificationTypeSound)、提示信息(UIUserNotificationTypeAlert)和无任何通知(UIUserNotificationTypeNone)。
你能够注册上诉四种通知类型的任意组合,但最终可用的通知形式须要根据用户对此 App 通知的设置肯定。好比:App 内部注册了角标、提示音和提示信息,可是用户关闭了声音通知,那么收到本地通知时是不会有提示音的。
对于 iOS8 及之后的系统,注册本地通知的代码示例以下:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 只有 iOS8 and later 才须要 if ([[UIApplication sharedApplication] respondsToSelector:@selector(registerForRemoteNotifications)]) { // 这里 types 能够自定义,若是 types 为 0,那么全部的用户通知均会静默的接收,系统不会给用户任何提示(固然,App 能够本身处理并给出提示) UIUserNotificationType types = (UIUserNotificationType) (UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert); // 这里 categories 可暂不深刻,本文后面会详细讲解。 UIUserNotificationSettings *mySettings = [UIUserNotificationSettings settingsForTypes:types categories:nil]; // 当应用安装后第一次调用该方法时,系统会弹窗提示用户是否容许接收通知 [[UIApplication sharedApplication] registerUserNotificationSettings:mySettings]; } // Your own other codes. return YES; }
当系统弹窗提示用户是否容许接收通知后,用户可能会拒绝;咱们能够在 AppDelegate 的 application:didRegisterUserNotificationSettings:
方法中用来查看注册成功的通知类型,咱们能够在拿到注册结果后作自定义操做(好比失败时弹个窗提示用户当前没法使用用户通知)。
苹果推荐在以后发送的本地通知时,要避免使用没有注册成功的通知类型(并非强制要求)。
- (void)application: (UIApplication*)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { if (notificationSettings.types & UIUserNotificationTypeBadge) { NSLog(@"Badge Nofitication type is allowed"); } if (notificationSettings.types & UIUserNotificationTypeAlert) { NSLog(@"Alert Notfication type is allowed"); } if (notificationSettings.types & UIUserNotificationTypeSound) { NSLog(@"Sound Notfication type is allowed"); } }
发送一个本地通知主要有以下步骤:
下面给出一段示例代码:
- (void)scheduleLocalNotification { NSDate *itemDate = [NSDate date]; UILocalNotification *localNotif = [[UILocalNotification alloc] init]; if (localNotif == nil) return; localNotif.fireDate = [itemDate dateByAddingTimeInterval:10]; localNotif.timeZone = [NSTimeZone defaultTimeZone]; localNotif.alertBody = [NSString stringWithFormat:NSLocalizedString(@"%@ after %i seconds scheduled.", nil), @"本地通知", 10]; localNotif.alertTitle = NSLocalizedString(@"Local Notification Title", nil); localNotif.soundName = UILocalNotificationDefaultSoundName; localNotif.applicationIconBadgeNumber = 1; NSDictionary *infoDict = [NSDictionary dictionaryWithObject:@"ID:10" forKey:@"LocalNotification"]; localNotif.userInfo = infoDict; [[UIApplication sharedApplication] scheduleLocalNotification:localNotif]; }
这里分三种状况讨论如何处理本地通知:
应用处于前台时,本地通知到达时,不会有提示音、通知栏横幅提示,可是 App 桌面图标的右上角角标是有数值显示的,因此即便在前台,咱们也应该对角标数量作处理
此时,咱们能够在 application:didReceiveLocalNotification: 方法中获取到本地通知,示例代码以下:
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { NSString *itemName = [notification.userInfo objectForKey:@"LocalNotification"]; [self.windowRootController displayNotification:[NSString stringWithFormat:@"%@ receive from didReceiveLocalNotificaition", itemName]]; // 这里将角标数量减一,注意系统不会帮助咱们处理角标数量 application.applicationIconBadgeNumber -= 1; }
当应用处于后台时,本地通知到达时,会根据本地通知设置的通知类型以及用户设置的通知类型进行提示,例如锁屏界面通知、通知栏通知、声音、角标。
此时若是滑动锁屏界面通知或点击通知栏通知,则会切换应用到前台,咱们可使用与应用处于前台时相同的获取通知的方式。
可是若是咱们点击 App 桌面图标,则没法获取到用户通知,此时通知栏消息仍然会存在。此外,角标也不会变化,若是但愿修改角标,则须要 App 进入前台后将其修改。
若是应用没有运行,当本地通知到达时,会根据本地通知设置的通知类型以及用户设置的通知类型进行提示,例如锁屏界面通知、通知栏通知、声音、角标。
此时若是滑动锁屏界面通知或点击通知栏通知,则会打开应用,但这时咱们获取通知的方式与前面有所不一样,经过 application:didReceiveLocalNotification: 是没法获取通知的。
这种状况咱们须要经过 application:didFinishLaunchingWithOptions: 中的 LaunchOptions 获取通知,示例代码以下:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { UILocalNotification *localNotif = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey]; if (localNotif) { NSString *itemName = [localNotif.userInfo objectForKey:@"LocalNotification"]; [self.windowRootController displayNotification:[NSString stringWithFormat:@"%@ receive from didFinishLaunch", itemName]]; // custom method [UIApplication sharedApplication].applicationIconBadgeNumber = localNotif.applicationIconBadgeNumber-1; } // Your own other codes. return YES; }
一样的,可是若是咱们点击 App 桌面图标,则没法获取到用户通知,此时通知栏消息仍然会存在。此外,角标也不会变化,若是但愿修改角标,则须要 App 进入前台后将其修改。
在 iOS8 及之后系统中,咱们能够定义一个与地理位置有关的本地通知,这样当咱们跨过设定的地理区域时,系统会发送本地通知。
请求用户容许使用定位服务:调用 CLLocationManager 的 =requestWhenInUseAuthorization=,
注意工程的 plist 中须要配置 NSLocationWhenInUseUsageDescription 选项,不然定位服务没法正常启用;示例代码以下:
- (void)registerLocationBasedNotification { CLLocationManager *locationManager = [[CLLocationManager alloc] init]; locationManager.delegate = self; // 申请定位权限 [locationManager requestWhenInUseAuthorization]; }
经过 CLLocationManagerDelegate 回调检查用户是否容许使用定位服务,若是容许了服务,那么能够发送一个位置相关的本地通知。
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status { // 由于上面咱们是使用了 requestWhenInUseAuthorization,因此这里检查的是 kCLAuthorizationStatusAuthorizedWhenInUse if (status == kCLAuthorizationStatusAuthorizedWhenInUse) { [self scheduleLocalBasedNotification]; } }
建立一个位置相关的本地通知,并将其交由系统处理。
- (void)scheduleLocalBasedNotification { UILocalNotification *locationNotification = [[UILocalNotification alloc] init]; locationNotification.alertBody = @"到达xxx"; locationNotification.regionTriggersOnce = NO; // 表示每次跨越指定区域就会发送通知 locationNotification.region = [[CLCircularRegion alloc] initWithCenter:LOC_COORDINATE radius:LOC_RADIUS identifier:LOC_IDENTIFIER]; [[UIApplication sharedApplication] scheduleLocalNotification:locNotification]; }
与上面讲过的 “处理收到的本地通知” 比较,这里能够在通知里获取到 region,而后能够作自定义操做,其他全部操做均与 “处理收到的本地通知” 一致。
注意若是用户没有容许使用定位权限,则没法收到位置相关的本地通知。
APNs 是苹果提供的远程通知的服务器,当 App 处于后台或者没有运行时,若是 App 的服务器(以后咱们称为 Provider)须要发送通知信息给客户端,则须要借助于 APNs 服务器。
使用 APNs 服务时,远程通知的路由路径为: Provider –> 苹果的 APNs 服务器 –> 手机设备 –> App。
在这个路径中,Provider 与 APNs 服务器之间有一个 TLS 链接,Provider 经过这个链接将远程通知推送到苹果的 APNs 服务器;
手机设备与 APNs 服务器之间也会有一个 TLS 链接,全部发往手机设备的 APNs 远程通知都是使用这一个 TLS链接,而后由设备区分远程通知所属的 App,进而通知给用户某应用有远程通知。
下面简单介绍下这个流程:
设备与 APNs 创建链接的过程如图:
须要明确的要点:
Provider 与 APNs 创建链接的过程如图:
须要明确的要点:
Feedback 是 APNs 服务器提供的用于减小服务器压力以及优化网络的服务,基本的工做流程以下图:
基本流程为:
注意:
注册远程通知的示例代码以下:
- (void)registerRemoteNotifications { // 区分是不是 iOS8 or later if ([[UIApplication sharedApplication] respondsToSelector:@selector(registerForRemoteNotifications)]) { // 这里 types 能够自定义,若是 types 为 0,那么全部的用户通知均会静默的接收,系统不会给用户任何提示(固然,App 能够本身处理并给出提示) UIUserNotificationType types = (UIUserNotificationType) (UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert); // 这里 categories 可暂不深刻,本文后面会详细讲解。 UIUserNotificationSettings *mySettings = [UIUserNotificationSettings settingsForTypes:types categories:nil]; // 当应用安装后第一次调用该方法时,系统会弹窗提示用户是否容许接收通知 [[UIApplication sharedApplication] registerUserNotificationSettings:mySettings]; } else { UIRemoteNotificationType types = UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound; [[UIApplication sharedApplication] registerForRemoteNotificationTypes:types]; } } - (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(nonnull UIUserNotificationSettings *)notificationSettings { // Register for remote notifications. [[UIApplication sharedApplication] registerForRemoteNotifications]; } // Handle register result. - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { //得到 device token,这一步处理为字符串的操做很重要 NSString *token = [[[deviceToken description] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]] stringByReplacingOccurrencesOfString:@" " withString:@""]; NSLog(@"DeviceToken string, %@", token); [UIApplication sharedApplication].applicationIconBadgeNumber = 0; // 将 token 发送给 Provider } - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { NSLog(@"Error in registration for apns service. Error: %@", error); }
Provider 发送给 APNs 服务器的内容格式以下:
{ // aps key 是必需要有的 "aps" : { "alert" : { "title" : "通知的概要,对 8.2 之前的系统本选项无效" "body" : "通知的具体内容", "loc-key" : "GAME_PLAY_REQUEST_FORMAT", "loc-args" : [ "Jenna", "Frank"] }, "badge" : 3, // 角标数值 "sound" : “chime.aiff" // 能够自定义提示音 }, "userName" : "username", // aps key 以外能够有自定义内容,须要符合 json 格式 "message" : ["hello", "world", "programmer"] }
有两种方式:
在 Provider 端进行本地化
App 能够将当前使用的语言发送给 Provider,Provider 在发送远程通知前,检查当前设备使用的语言,并作好本地化后发送给 APNs 服务器。App 发送当前使用的语言给 Provider 的示例代码:
NSString *preferredLang = [[NSLocale preferredLanguages] objectAtIndex:0]; const char *langStr = [preferredLang UTF8String]; [self sendProviderCurrentLanguage:langStr]; // custom method
通常来讲,将当前系统语言信息发送给 Provider 时,也会将 Token 一块儿发送,这样 Provider 才可以在发送远程通知时根据不一样目的设备进行本地化处理。
此外,当应用启动后,用户可能会修改系统语言,这时,App 须要监听 NSCurrentLocaleDidChangeNotification 通知,并在处理通知的方法中从新向 Provider 发送当前使用的语言。
在客户端本地化
这种模式下,Provider 在发送远程通知时,须要设置 Payload -> alert 中的本地化相关属性,以下:
{ // aps key 是必需要有的 "aps" : { "alert" : { "title" : "通知的概要,对 8.2 之前的系统本选项无效", "loc-key" : "Remote Notification", "loc-args" : [ "hello", "world"] }, "badge" : 3, // 角标数值 "sound" : “chime.aiff" // 能够自定义提示音 } }
上面 loc-key 以及 loc-args 就是本地化相关的属性,用于本地化 alert 中的 body。
当 App 收到此消息时,会根据系统当前的语言设置去相应的本地化文件中查找与 loc-key 对应的 value,若是 loc-key 对应的 value 是一个格式化的字符串,那么能够用 loc-args 传递参数。
假设本地化文件中: "Remote Notification" = "咱们程序员一般钟爱:%@ %@" ,那么提示信息就是: "咱们程序员钟爱:hello world";
此外在本地化文件中咱们也能够用 %n$@ 代替 %@ 用来表示使用 loc-args 的第几个参数。
例如:"Remote Notification" = "咱们程序员一般钟爱:%2$@ %1$@",那么提示信息是:"咱们程序员钟爱:world hello"。
一样的,title-loc-key 和 title-loc-args 是对 alert 中的 title 作本地化的。
这里分三种状况讨论如何处理远程通知:
应用处于前台时,本地通知到达时,不会有提示音、通知栏横幅提示。可是 App 桌面图标的右上角角标是有数值显示的,因此即便在前台,咱们也应该对角标数量作处理;
此时,咱们能够在 application:didReceiveRemoteNotification:fetchCompletionHandler: 方法中获取到远程通知,示例代码以下:
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler { NSData *infoData = [NSJSONSerialization dataWithJSONObject:userInfo options:0 error:nil]; NSString *info = [[NSString alloc] initWithData:infoData encoding:NSUTF8StringEncoding]; [self.windowRootController displayNotification:[NSString stringWithFormat:@"From didReceiveRemoteNotification: %@", info]]; // 这里将角标数量减一,注意系统不会帮助咱们处理角标数量 application.applicationIconBadgeNumber = notification.applicationIconBadgeNumber - 1; }
当应用处于后台时,远程通知到达时,会根据注册通知是设置的通知类型以及用户设置的通知类型进行提示,例如锁屏界面通知、通知栏通知、声音、角标。
此时若是滑动锁屏界面通知或点击通知栏通知,则会切换应用到前台,咱们可使用与应用处于前台时相同的获取通知的方式。
可是若是咱们点击 App 桌面图标,则没法获取到用户通知,此时通知栏消息仍然会存在。此外,角标也不会变化,若是但愿修改角标,则须要 App 进入前台后将其修改。
这里有两种处理方式:
与本地通知的处理方式相同,在 application:didFinishLaunchingWithOptions: 的 LaunchOptions 中获取通知,不过内部代码会略有不一样,示例以下:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSDictionary *remoteNotif = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey]; if (remoteNotif) { NSData *infoData = [NSJSONSerialization dataWithJSONObject:remoteNotif options:0 error:nil]; NSString *info = [[NSString alloc] initWithData:infoData encoding:NSUTF8StringEncoding]; [self.windowRootController displayNotification:[NSString stringWithFormat:@"From didFinishLaunch: %@", info]]; [UIApplication sharedApplication].applicationIconBadgeNumber -= 1; } // Your own other codes. return YES; }
与应用处于先后台时处理方式相同,使用 application:didReceiveRemoteNotification:fetchCompletionHandler: 方法,示例代码见 "应用处于前台" 时的处理。
对于远程通知,推荐使用此种方式处理。
此外,对于远程通知,若是咱们点击 App 桌面图标,则没法获取到用户通知,此时通知栏消息仍然会存在。此外,角标也不会变化,若是但愿修改角标,则须要 App 进入前台后将其修改。
静默推送是指应用在前台或后台状态下,收到远程通知时,没有弹窗或横幅提示,即便处于后台也能够处理远程通知。具体使用流程以下:
在 Provider 发送远程通知时,须要将远程通知 Payload 中的 aps 内的 content-available 设置为 1,以下:
aps { content-available: 1 alert: {...} }
应用须要实现 application:didReceiveRemoteNotification:fetchCompletionHandler: 方法接收静默推送。
有几点须要注意:
首先须要注意的是,可操做通知只适用于 iOS8 及之后的系统。
可操做通知其实并非一种新的通知形式,它只是在这本地通知和远程通知的基础上加了一些可操做的行为而已。为了直观说明什么是可操做通知,能够参考下图:
可操做通知为用户提供了在通知提示中方便执行操做的方式,在使用横幅提示通知消息时,最多能够有两个操做,在使用弹窗提示通知消息是,最多能够有四个操做。下面讲解如何使用:
基本使用方法:
建立一个 UIMutableUserNotificationAction 对象,并按需求配置该对象的属性,示例代码:
UIMutableUserNotificationAction *acceptAction = [[UIMutableUserNotificationAction alloc] init]; // 为该操做设置一个 id acceptAction.identifier = @"accept"; // 设置该操做对应的 button 显示的字符串 acceptAction.title = @"Accept"; // 指定是否须要应用处于运行状态 acceptAction.activationMode = UIUserNotificationActivationModeBackground; // 表示该操做是否有害,若设置为 YES,则对应的button会有高亮 acceptAction.destructive = NO; // 当锁屏时收到可操做通知,该属性表示是否必须解锁才能执行该操做 acceptAction.authenticationRequired = YES;
建立一个 UIMutableUserNotificationCategory 对象,并将自定义的操做经过 setActions: 的方式设置给 category 对象。代码以下:
// 这里为了测试,又新建了两个 action,declineAction 和 maybeAction ,代码可见 demo UIMutableUserNotificationCategory *inviteCategory = [[UIMutableUserNotificationCategory alloc] init]; // 设置一个 ID,用于本地通知或远程通知时指定该通知可执行的操做group inviteCategory.identifier = @"Action"; // 为弹窗模式设置 actions [inviteCategory setActions:@[acceptAction, maybeAction, declineAction] forContext:UIUserNotificationActionContextDefault]; // 为横幅模式设置 actions [inviteCategory setActions:@[acceptAction, declineAction] forContext:UIUserNotificationActionContextMinimal];
注册通知类型以及可操做的actions
相似于本地通知和远程通知,调用 registerUserNotificationSettings: 注册通知,只是这里的 setting 加入了咱们上面定义的 category。
NSSet *categories = [NSSet setWithObjects:inviteCategory, nil]; // 这里 types 能够自定义,若是 types 为 0,那么全部的用户通知均会静默的接收,系统不会给用户任何提示(固然,App 能够本身处理并给出提示) UIUserNotificationType types = (UIUserNotificationType) (UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert); // 这里 categories 可暂不深刻,本文后面会详细讲解。 UIUserNotificationSettings *mySettings = [UIUserNotificationSettings settingsForTypes:types categories:categories]; // 当应用安装后第一次调用该方法时,系统会弹窗提示用户是否容许接收通知 [[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];
若是将可执行通知用于远程通知,那么须要按照远程通知的注册方式获取 token,可参考远程通知的注册。
以前说过,可操做通知只是在本地通知和远程通知的基础上加了自定义的操做,因此发送可操做通知就是发送本地通知或远程通知。
不过,若是但愿咱们自定义的 action 有效,在发送本地通知或远程通知时须要进行一些改变:
本地通知的可操做通知
为 UILocalNotification 对象设置咱们自定义的 category。以下:
UILocalNotification *notification = [[UILocalNotification alloc] init];
// Other configurations
notification.category = @"Action";
[[UIApplication sharedApplication] scheduleLocalNotification:notification];
远程通知的可操做通知
在远程通知的 Payload 中设置咱们自定义的 category,以下:
{
"aps" : { "alert" : "You’re invited!", "category" : "Action" }
}
处理可操做通知与处理本地通知和远程通知相同,惟一的不一样点就是当用户执行了某个操做后,
应用能够在后台运行 application:handleActionWithIdentifier:forRemoteNotification:completionHandler: 处理通知(例如后台更新数据等操做),咱们能够在这个回调里快速的执行操做:
- (void)application:(UIApplication *) application handleActionWithIdentifier: (NSString *) identifier // either forLocalNotification: (NSDictionary *) notification or forRemoteNotification: (NSDictionary *) notification completionHandler: (void (^)()) completionHandler { if ([identifier isEqualToString: @"accept"]) { [self handleAcceptActionWithNotification:notification]; } // 执行自定义代码完成后必须调用 completionHandler(); }
对于本地通知咱们可使用 application:handleActionWithIdentifier:forLocalNotification:completionHandler: 。
这里举个例子说明:
假如A向B发出了一个出席发布会邀请,而且 App 是以远程通知的方式接收到该信息,那么当不使用可操做通知的时候,咱们须要作的事情主要包括:
那么,若是咱们使用可操做通知,能够很简单的作到这件事情:
邀请通知处理完毕。
能够看到,不管是从用户角度仍是开发者角度,可操做通知都极大的方便了处理具备可操做动做的这类通知。
到这里已经讲解完成了用户通知的内容,文章包含了苹果给出的用户通知的基本全部用法.