当应用不在前台运行时,可以通过 user notifications 推送信息给用户。user notifications 包括 local notifications 和 remote notifications 两种类型。操作系统展示本地和远程通知的方式是一样的,包括展示提醒信息,应用图标标记和声音。当用户收到通知时,可打开应用去查看详细信息。
本地通知和远程通知最基本的区别是:
- 本地通知由应用本身安排和推送到本机。
- 远程通知由服务器发送到APNs(Apple Push Notification service),再由其推送到设备。
使用 User Notification 第一步,注册通知类型
从iOS8开始,不管是本地还是远程通知,如果应用想使用该功能,必须先注册希望接收的通知类型。
UIUserNotificationType userNotificationTypes = UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound;
UIUserNotificationSettings *userNotificationSettings = [UIUserNotificationSettings settingsForTypes:userNotificationTypes categories:nil];
[[UIApplication sharedApplication] registerUserNotificationSettings:userNotificationSettings];
如果不注册,系统将不推送任何类型的通知,即使应用在前台运行也不会触发 AppDelegate 的application:didReceiveLocalNotification:
方法,在系统设置中也没有设置该应用的通知选项。
当第一次执行注册代码时,系统会弹出一个警告窗口,告诉用户该应用希望推送通知,用户可以选择是否允许。使用不同的类型组合再次注册,则系统设置中也会相应改变,未注册的类型在系统设置中也就没有相应选项了。如果展示通知的类型设为UIUserNotificationTypeNone,那么之后在系统设置中设置该应用的通知选项将消失,并且即使改变展示通知的类型再去注册也没有了效果。
注册之后系统会调用 AppDelegate 的application:didRegisterUserNotificationSettings:
方法,传来的 UIUserNotificationSettings 参数指明了当前用户允许的通知展示类型。可以通过[UIApplication sharedApplication].currentUserNotificationSettings
随时查看当前用户允许的通知展示类型。
所以,如果没有特殊情况,应该直接注册所有的通知展示类型。
Local Notification
本地通知是实现基于时间的行为的理想方式,例如日历事件,提醒事项等。每一个应用最多只能同时安排64条本地通知,若此时安排新的本地通知会被系统丢弃,重复安排的通知按同一条计算。
当应用收到本地通知时在前台,系统会调用 AppDelegate 中的application:didReceiveLocalNotification:
方法,当应用在后台挂起或关闭时则不触发该方法。但是当应用在后台挂起时,用户通过点击或滑动提醒信息进入应用时,会调用application:didReceiveLocalNotification:
方法,若用户通过点击应用图标进入应用则不会调用。若应用已关闭,则都不会调用application:didReceiveLocalNotification:
方法,用户在应用关闭状态下通过本地通知打开应用,可以在application:didFinishLaunchingWithOptions:
方法中获得该通知,如果通过点击应用图标打开则无法获得
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
UILocalNotification *localNotification = launchOptions[UIApplicationLaunchOptionsLocalNotificationKey];
}
构建一个UILocalNotification:
UILocalNotification *localNotification = [[UILocalNotification alloc] init];
// fireDate设置推送通知的日期和时间,受timeZone属性的影响,同时设置region会发生异常
localNotification.fireDate = [NSDate dateWithTimeIntervalSinceNow:5];
// timeZone设置时区,对fireDate有影响。默认为nil,此时fireDate将根据GMT时间
localNotification.timeZone = [NSTimeZone defaultTimeZone];
// repeatIntervar设置重复推送的间隔,默认为0,不重复推送。
localNotification.repeatInterval = kCFCalendarUnitMonth;
// repeatCalendar设置重复推送时使用的历法,默认为nil,则使用[NSCalendar currentCalendar]
localNotification.repeatCalendar = [NSCalendar currentCalendar];
// region设置推送通知的地区,同时设置fireDate会发生异常
localNotification.region = nil;
// regionTriggersOnce设置是否只在第一次到达地区边界时推送消息
localNotification.regionTriggersOnce = NO;
// alertBody设置通知要提醒的信息
localNotification.alertBody = @"alertBody";
// alertAction设置滑动动作的标题
localNotification.alertAction = @"alertAction";
// alertTitle设置通知提醒的标题
localNotification.alertTitle = @"alertTitle";
// hasAction设置是否展示动作按钮
localNotification.hasAction = YES;
// alertLaunchImage设置动作触发时应用启动图片
localNotification.alertLaunchImage = @"default";
// applicationIconBadgeNumber设置显示在应用图标上的未读信息数量
localNotification.applicationIconBadgeNumber = 1;
// soundName设置通知的提示声音的文件
localNotification.soundName = UILocalNotificationDefaultSoundName;
// userInfo设置通知的自定义信息
localNotification.userInfo = @{};
[[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
除了将通知加入列表外,应用还可以通过 UIApplication 对象的presentLocalNotificationNow:
方法立即推送通知,也可以通过cancelLocalNotification:
方法和cancelAllLocalNotifications
方法来取消已安排的通知,同时这些方法可以使正在展示的通知从屏幕上消失。
注意:
在使用关于位置的通知时,一定要指定CLRegion的identifier,CLRegion靠identifier来唯一区别,当通知对象的区域对象的identifier为空时,该通知对象最终无法被安排到通知队列中。拥有含相同identifier的区域对象的通知对象只有一个会被安排到队列中。
CLLocationCoordinate2D locationCoordinate = CLLocationCoordinate2DMake(0, 0);
NSString *regionIdentifier = [NSString stringWithFormat:@"%g%g", locationCoordinate.latitude, locationCoordinate.longitude];
CLRegion *region = [[CLCircularRegion alloc] initWithCenter:coordinate radius:100 identifier:regionIdentifier];
注册远程通知
如果应用希望接收服务器发送的远程通知,需要通过调用 UIApplication 对象的registerForRemoteNotifications
方法向APNs(Apple Push Notification service)注册。当注册成功时,应用会调用代理对象的application:didRegisterForRemoteNotificationsWithDeviceToken:
方法并传入一个 device token(二进制编码),该 device token 需要发送给发送远程通知的服务器。如果注册失败,则会调用代理对象的application:didFailToRegisterForRemoteNotificationsWithError:
方法。注意,device token 是可变的,所以每次应用启动都需要重新注册。若设备处于未联网状态,则以上两个方法都不会被调用。
处理本地和远程通知
当收到通知时,如果应用并没有在前台运行,那么根据用户的设置,系统可以通过显示提醒,应用图标加角标,声音以及其他的动作按钮来展示应用。当用户点击了自定义的动作按钮,应用代理对象会调用application:handleActionWithIdentifier:forRemoteNotification:completionHandler:
方法或者application:handleActionWithIdentifier:forLocalNotification:completionHandler:
方法。当用户点击默认的提醒栏时,如果应用未启动,那么会调用代理对象的application:didFinishLaunchingWithOptions:
方法,在 options 字典中,可以通过键值UIApplicationLaunchOptionsLocalNotificationKey
获取触发应用启动的 UILocalNotification 对象,或者通过键值UIApplicationLaunchOptionsRemoteNotificationKey
获取远程通知的userInfo(NSDictionary对象),如果触发应用启动的是远程通知,那么系统还会调用代理对象的application:didReceiveRemoteNotification:fetchCompletionHandler:
方法并且执行顺序先与application:didFinishLaunchingWithOptions:
方法。当用户通过点击应用图标启动应用时,则无法获得任何有关通知的信息。当应用在前台运行时收到通知,则会调用代理对象的application:didReceiveLocalNotification:
方法或application:didReceiveRemoteNotification:fetchCompletionHandler:
方法,还有一个方法是application:didReceiveRemoteNotification:
,该方法只有应用在前台时才会被调用,但若application:didReceiveRemoteNotification:fetchCompletionHandler:
方法被实现则会替代该方法,其不会被调用。当应用在后台挂起时收到远程通知,如果此时 Background Mode 设置了远程通知,那么应用会被唤醒并在后台调用application:didReceiveRemoteNotification:fetchCompletionHandler:
方法,但是有30秒的时间限制,另外如果设置中的后台刷新选项被关闭同时通知选项被设为不允许通知,则也不会调用该方法。
有关处理方法中的最后一个参数 completionHandler,当执行完处理通知的代码后,一定要调用传入的 block 参数 completionHandler,否则会使应用结束运行。在异步获取数据结束后,执行application:didReceiveRemoteNotification:fetchCompletionHandler:
方法中的 completionHandler 时还要传入一个描述获取数据结果的参数(UIBackgroundFetchResult类型)。
使用通知处理(Notification Actions)
从 iOS8 开始,用户对于通知的处理除了默认的方式(点击横幅或提醒中的默认处理,锁屏时滑动通知)外,还可以自定义其他处理方式供用户选择。在横幅,锁屏状态或者通知中心中最多可添加两种自定义处理,在提醒框的选项中最多可添加4种自定义处理。使用自定义通知处理的第一步就是注册处理。
UIMutableUserNotificationAction *acceptAction = [[UIMutableUserNotificationAction alloc] init];
// 当用户收到通知并选择该处理,系统会将 identifier 传给应用代理对象的相应方法,以便判断用户选择了哪一项处理。
acceptAction.identifier = @"ACTION_ACCEPT";
// 处理按钮的标题。
acceptAction.title = @"Accept";
// 设置当用户点击处理按钮后,应用在前台运行还是后台运行,如果为后台模式,应用将获得一定的时间运行,若此时应用在前台(锁屏情况下),应用将继续保持在前台。若该处理不需要用户与界面交互则可用后台模式。
acceptAction.activationMode = UIUserNotificationActivationModeBackground;
// 设为YES则按钮背景色为红色,起强调作用。
acceptAction.destructive = NO;
// 该设置针对锁屏情况下,用户选择处理后是否需要输入密码,若 activationMode 为后台模式则输入密码不会解锁,只是执行处理,若为前台模式,则必须验证密码,无论该属性的值是什么。
acceptAction.authenticationRequired = YES;
UIMutableUserNotificationAction *maybeAction = [[UIMutableUserNotificationAction alloc] init];
maybeAction.identifier = @"ACTION_MAYBE";
maybeAction.title = @"Maybe";
maybeAction.activationMode = UIUserNotificationActivationModeBackground;
maybeAction.destructive = NO;
maybeAction.authenticationRequired = NO;
UIMutableUserNotificationAction *declineAction = [[UIMutableUserNotificationAction alloc] init];
declineAction.identifier = @"ACTION_DECLINE";
declineAction.title = @"Decline";
declineAction.activationMode = UIUserNotificationActivationModeBackground;
declineAction.destructive = YES;
declineAction.authenticationRequired = NO;
// 需要将定义的一组 Action 放入到一个category中,在推送通知设置该通知对应的 category,当系统收到通知展示时通过 category 的 identifier 匹配到已注册的 category,并将其中的 action 展示出来。
UIMutableUserNotificationCategory *inviteCategory = [[UIMutableUserNotificationCategory alloc] init];
inviteCategory.identifier = @"CATEGORY_INVITE";
// 设置 category 的 action 时对应两种 context,default 和 minimal。default 应用于展示4个 action 的地方,minimal 应用于展示2个 action 的地方。若未指定 minimal,则只能展示2个 action 的地方将展示 default 中的前两个。
[inviteCategory setActions:@[acceptAction, maybeAction, declineAction] forContext:UIUserNotificationActionContextDefault];
[inviteCategory setActions:@[acceptAction, declineAction] forContext:UIUserNotificationActionContextMinimal];
// 可以注册多个 category,所以注册前要将所有 category 放入一个 set 中用于注册。
NSSet *categories = [NSSet setWithObjects:inviteCategory, nil];
UIUserNotificationType userNotificationTypes = UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound;
UIUserNotificationSettings *userNotificationSettings = [UIUserNotificationSettings settingsForTypes:userNotificationTypes categories:categories];
[[UIApplication sharedApplication] registerUserNotificationSettings:userNotificationSettings];
当推送通知时,只要设置了远程通知的 key 值 category 或者本地通知的 category 属性为已注册的 category 的 identifier,那么当系统展示通知时,就会将相应处理按钮展示出来。最后,当用户点击了自定义的处理按钮,系统会调用应用代理对象的application:handleActionWithIdentifier:forRemoteNotification:completionHandler:
方法或application:handleActionWithIdentifier:forLocalNotification:completionHandler:
方法。在该方法中可以通过传入的 action 的 identifier 来判断用户点击了哪一个 action 按钮,另还传入了通知对象。
判断用户是否允许通知
可通过[UIApplication sharedApplication].currentUserNotificationSettings
获取用户的设置并进行判断,但要注意该属性只适用于 iOS8 及以后,需要处理较早版本的情况。
BOOL enable = NO;
if ([[UIDevice currentDevice].systemVersion floatValue] >= 8.0f) {
UIUserNotificationSettings *settings = [[UIApplication sharedApplication] currentUserNotificationSettings];
enable = UIUserNotificationTypeNone == settings.types ? NO : YES;
} else {
UIRemoteNotificationType type = [[UIApplication sharedApplication] enabledRemoteNotificationTypes];
enable = UIRemoteNotificationTypeNone == type ? NO : YES;
}
若用户未开启推送功能,可通过以下代码跳转至设置中有关该应用的界面
UIApplication *application = [UIApplication sharedApplication];
NSURL *openSettingsURL = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
if ([application canOpenURL:openSettingsURL]) {
if ([application respondsToSelector:@selector(openURL:options:completionHandler:)]) {
[application openURL:openSettingsURL options:@{} completionHandler:nil];
} else {
[application openURL:openSettingsURL];
}
} else {
// Do something.
}