iOS中存在三种常见的事件通知方式:NSNofiticationCenter、KVO Notification 和 User Notifications,其中 User Notifications,就是本文将要探讨的用户通知。
我们都知道 iOS 系统经常的有一些与 App 相关的通知栏消息,这些消息往往伴随着提示音以及 App 的桌面图标右上角的未读消息提示,这些通知就是 iOS 的用户通知。
用户通知分为两类:本地通知和远程通知,其中远程通知又称为推送通知。
两者最主要的区别是:本地通知是由 App 发送到当前设备上,不需要网络支持;而远程通知是由 App 的服务器发送到苹果的 APNs 服务器,并由 APNs 服务器转发到相应设备(由 App 服务器指定接收通知的设备)。
两者最主要的共同点是:本地通知和远程通知对用户的表现形式是相同的,两者均可以采用通知栏消息、App 桌面图标右上角角标和提示音的方式通知用户。
及时有效的(无论是在前台还是后台)向用户发送消息(聊天信息、新闻、待办事项、天气变化等)是用户通知最大的优势。
此外,有效合理的使用用户通知,可以让我们的 App 有更好的体验,如:
本文后续内容将以应用开发者的角度对用户通知进行深入的探讨,本文讨论内容针对iOS7/8/9,有关 iOS10 系统的用户通知会另做讲解。
本文中的远程通知使用了 Simplepush.php ,内部代码很简单,可使用该脚本自定义远程通知的内容, 建议花几分钟查看以下 Simplepush.php 的用法。此外,demo 不提供证书,如有远程通知需求,请自行申请证书,否则无法正常使用 Simplepush。
本文主要参考了苹果官方的 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");
}
}
发送一个本地通知主要有如下步骤:
UILocalNotification
对象;UILocalNotification
对象的 fireDate 属性,该属性表示什么时间点发送这条本地通知; 同时可以设置 timeZone 属性表示时区,设置 timeZone 后,当用户跨越时区时,fireDate 会按照时区被调整(类似于钟表调整); 此外,可以使用 repeatInterval 和 repeatCalendar 来设置周期性的通知。NSLocalizedString(@"This is alert body", nil);
。 注意 alertTitle 属性只适用于 iOS8.2 及以后的系统[[UIApplication sharedApplication] currentUserNotificationSettings]
获取注册成功的通知类型)。UILocalnotification
放入通知队列中:使用方法 scheduleLocalNotification:
会按照 UILocalnotification
中的 fireDate 进行通知的发送, 而使用 presentLocalNotificationNow:
会立即发送该本地通知。
下面给出一段示例代码:
- (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 服务器提供的用于减少服务器压力以及优化网络的服务,基本的工作流程如下图: