本地推送/本地通知

一、本地推送/本地通知 是什么?

  1. (名称概念)本地推送,其实也就是本地通知,它们指的是同一种概念,只是叫法不同,下文统一称呼为本地通知(还有时候会叫做“推送本地通知”,真搞不懂为啥这么多叫法……)。本地通知是由系统触发的,它是基于将来时间行为的一种通知形式,本地通知并不依赖于网络连接,可简单将其视为一个定时装置即可。

  2. (常用场景)常用于定期提醒行为,如一些 TODO 类的App定期提醒用户完成任务,健身锻炼类的App的每天锻炼提醒,又或者一个应用在一段时候后不使用通常会提示用户使用此应用等。

  3. (形式区别)本地通知与远程推送RemoteNotification通知形式相似,不同的是,本地通知并不依赖于网络连接,而远程推送则一定是通过苹果服务器将消息推送至已注册的设备上的,同时本地通知样式一般较为固定,通常是先设置好推送周期,而且推送内容往往也是固定的,可存放于plist文件中。

  4. 本地通知类型有两个

    • Date-based类型:即按给定的日期触发通知,通常需要考虑 Time Zone 的问题。
    • Region-based类型:即按给定区域触发通知,当用户进入或离开指定 region 时触发通知

二、本地通知使用

1. 在 iOS 10之前

相比iOS 8之前的本地通知(简单设置通知的声音,app 的 badge 和 alerts 的内容等),iOS 8之后通知实例具备了category 和 action 的概念。iOS 8之后通知还涉及新增的三个类,分别为 UIUserNotificationSettingsUIUserNotificationCategoryUIUserNotificationAction 以及Category 和 Action 的可变版本UIMutableUserNotificationAction、UIMutableUserNotificationCategory。

  1. 本地推送使用的类是UILocalNotification
    而在 iOS 8 之后,使用UILocalNotification前需要调用-[UIApplication registerUserNotificationSettings:]方法注册用户通知权限。(多个通知只需一次, 建议放在AppDelegate 的didFinishLaunchingWithOptions方法中)

申请获取权限图:


  1. 创建一个本地通知通常分为以下几个步骤:
    1. 创建UILocalNotification。
    2. 设置处理通知的时间fireDate。
    3. 配置通知的内容:通知主体、通知声音、图标数字等。
    4. 配置通知传递的自定义数据参数userInfo(可选)。
    5. 调用通知,可以使用scheduleLocalNotification:按计划调度一个通知,也可以使用presentLocalNotificationNow立即调用通知。

整体测试 demo 代码,包括对应属性使用的描述和注意点:

//
//  AppDelegate.m
//  Notifications
//
//  Created by Jacob_Liang on 2017/9/19.
//  Copyright © 2017年 Jacob. All rights reserved.
//

#import "AppDelegate.h"


static NSString * const kIGNOREKEY = @"IGNOREKEY";
static NSString * const kOPENACTIONKEY = @"OPENACTIONKEY";
static NSString * const kCATEGORYKEY = @"ALERTCATEGORY";

@interface AppDelegate ()


@end

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    //如果已经获得发送通知的授权则创建本地通知,否则请求授权(注意:如果不请求授权在设置中是没有对应的通知设置项的,也就是说如果从来没有发送过请求,即使通过设置也打不开消息允许设置)
    if ([[UIApplication sharedApplication] currentUserNotificationSettings].types != UIUserNotificationTypeNone) {
        [self addLocalNotification];
    } else {
        //iOS 8 请求用户通知权限
        if ([UIApplication instancesRespondToSelector:@selector(registerUserNotificationSettings:)]) {
            
            //添加通知的动作
            UIMutableUserNotificationCategory *category = [self addLocalNotificationActions];
            
            UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeBadge | UIUserNotificationTypeAlert | UIUserNotificationTypeSound categories:[NSSet setWithObject:category]];
            [application registerUserNotificationSettings:settings];
            //在请求权限弹出的 Alert 选择中,用户选择 "好"时,会回调 application:didRegisterUserNotificationSettings:方法
        }
    }
    
    /*
     iOS 10 之前点击本地通知,从后台唤醒或启动 App 时在这个方法的 Options里获取 本地通知的 UserInfo;
     */
    UILocalNotification *notification = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];
    NSLog(@"%s  -- %@", __func__, notification);
    
//    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//        [self showInfo:[NSString stringWithFormat:@"%s  -- %@", __func__, notification]];
//    });
    
    //test app teminate 后,响应本地通知的 backgroundmode 的 acton后,再次启动,如果有打印即证明,backgroundmode 的 acton的触发真的没有启动 App,但是会回调 Action 的方法;
    NSString *clickIgnoreActionStr = [[NSUserDefaults standardUserDefaults] objectForKey:kIGNOREKEY];
    if (clickIgnoreActionStr.length) {
        NSLog(@"%@",clickIgnoreActionStr);
        [[NSUserDefaults standardUserDefaults] setObject:nil forKey:kIGNOREKEY];
    }
    
    
    return YES;
}

#pragma mark - 添加本地通知
- (void)addLocalNotification {
    
    //定义本地通知对象
    UILocalNotification *notification = [[UILocalNotification alloc] init];
    //设置时区
    notification.timeZone = [NSTimeZone defaultTimeZone];
    //设置调用时间
    notification.fireDate = [NSDate dateWithTimeIntervalSinceNow:10.0];//通知触发的时间,10s以后
    notification.repeatInterval = 0;//通知重复间隔,其是一个 option 值, 0表示不重复,即 fire 之后就 discard 该 notification,即不会被copy 进scheduledLocalNotifications数组里。
    //notification.repeatCalendar = [NSCalendar currentCalendar];//当前日历,使用前最好设置时区等信息以便能够自动同步时间
    
    //设置通知属性
    notification.alertBody = @"最近添加了诸多有趣的特性,是否立即体验?"; //通知主体
    notification.applicationIconBadgeNumber = 1;//应用程序图标右上角显示的消息数
    notification.alertAction = @"打开应用"; //待机界面的滑动动作提示
    notification.alertLaunchImage = @"Default";//通过点击通知打开应用时的启动图片,这里使用程序启动图片
    //notification.soundName = UILocalNotificationDefaultSoundName;//收到通知时播放的声音,默认消息声音
    notification.soundName = @"msg.caf";//通知声音(需要真机才能听到声音)
    
    //设置用户信息
    notification.userInfo = @{@"id":@1234, @"user":@"Jacob"};//绑定到通知上的其他附加信息
    //设定该通知的actions,actions确保已经添加到 category , 每一个 category 表示一种类型的 actions,也就说可以有很多类型的 category。但是 category 需要提前注册到 setting 中。
    notification.category = kCATEGORYKEY;
    
    //调用通知
    [[UIApplication sharedApplication] scheduleLocalNotification:notification]; //scheduleLocalNotification 方法会对 notification 对象进行 copy
    
    //    [[UIApplication sharedApplication] presentLocalNotificationNow:notification]; //立即发送本地通知,无视 notification 的 fireDate 属性值,会调用 application:didReceiveLocalNotification:处理通知
    
}

#pragma mark - 添加通知的动作
//添加通知的动作
- (UIMutableUserNotificationCategory *)addLocalNotificationActions {
    //UIMutableUserNotificationAction用来添加自定义按钮
    UIMutableUserNotificationAction * responseAction = [[UIMutableUserNotificationAction alloc] init];
    responseAction.identifier = kOPENACTIONKEY;
    responseAction.title = @"打开应用";
    responseAction.activationMode = UIUserNotificationActivationModeForeground; //点击的时候启动程序
    
    UIMutableUserNotificationAction *deleteAction = [[UIMutableUserNotificationAction alloc] init];
    deleteAction.identifier = kIGNOREKEY;
    deleteAction.title = @"忽略";
    deleteAction.activationMode = UIUserNotificationActivationModeBackground; //点击的时候不启动程序,后台处理
    deleteAction.authenticationRequired = YES;//需要解锁权限
    deleteAction.destructive = YES; //YES为红色,NO为蓝色
    
    UIMutableUserNotificationCategory *category = [[UIMutableUserNotificationCategory alloc] init];
    category.identifier = kCATEGORYKEY;//用于将该 category 标识的同时,那一个 notification 实例需要使用这个 category 的 actions 也是传入这个值给 notification 的。
    //UIUserNotificationActionContextDefault:默认添加可以添加两个自定义按钮
    //UIUserNotificationActionContextMinimal:四个自定义按钮
    [category setActions:@[responseAction, deleteAction] forContext:UIUserNotificationActionContextDefault];
    
    return category;
}

//iOS 8 ~ 9 ,当点击本地通知自定义的响应按钮(action btn)时,根据按钮的 activeMode 模式,回调以下方法
//1. ActivationModeForeground 的 action , 会启动 App 同时回调方法
//2. ActivationModeBackground 的 action 不启动 App 让 App 在 background 下回调方法
- (void)application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier forLocalNotification:(UILocalNotification *)notification completionHandler:(void (^)())completionHandler {
    
    if ([identifier isEqualToString:kOPENACTIONKEY]) {
        //ActivationModeForeground 的 action , 启动 App 让 App 在 Foreground 下响应
        
        [self showInfo:[NSString stringWithFormat:@"thread -%@\n identifier -%@", [NSThread currentThread], identifier]];
        
    } else {
        
        //ActivationModeBackground 的 action 不启动 App 让 App 在 background 下响应
        NSLog(@"%s  -- %@  -- identifier %@ --- thread %@", __func__, notification, identifier, [NSThread currentThread]);
        
        //下面代码用于测试,退出 App 后接收到 本地通知时,点击后台action时是否执行了这个响应方法。实测执行了的
        [[NSUserDefaults standardUserDefaults] setObject:@"ActivationModeBackground 的 action 不启动 App 让 App 在 background 下响应" forKey:@"IGNOREKEY"];
        [[NSUserDefaults standardUserDefaults] synchronize];
        
    }
    
    
    
    completionHandler(); //根据Action btn 的 identifier 处理自定义事件后应该马上调用 completionHandler block,如果调用 completionHandler block 失败的话,App 会立即 terminated。
}


// APP在前台运行中收到 本地通知 时调用, 以及App 处于后台挂起(suspended)状态,但未 terminated 时,点击通知启动都是这个方法进行响应
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {
    // 可根据notification对象的userInfo等属性进行相应判断和处理
    NSLog(@"%s --- %@", __func__, notification);
}

//调用过用户注册通知方法之后执行(也就是调用完registerUserNotificationSettings:方法之后执行)
-(void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings {
    
    if (notificationSettings.types != UIUserNotificationTypeNone) {
        [self addLocalNotification];
    }
}



- (void)applicationWillEnterForeground:(UIApplication *)application {
    
    [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];//进入前台取消应用消息图标
    
    //获取本地通知数组
    NSArray *notifications = [[UIApplication sharedApplication] scheduledLocalNotifications];
    for (int i = 0; i < notifications.count; i++) {
        UILocalNotification *notificaiton = notifications[i];
        NSLog(@"%@ \n", notificaiton);
    }
}



#pragma mark - 移除本地通知,在不需要此通知时记得移除
- (void)removeNotification {
    
    //获取本地通知数组 (该数组会持有需要重复 fired 的 已被 copy 的 notification 对象,用于到达下次间隔时再 fire, 如果不需要重复的 notification,即 notification.repeatInterval = 0 的话,该 notification fire 之后不会被 copy 保留到这个数组里)
    //本地通知最多只能有64个,超过会被系统忽略
    NSArray *notifications = [[UIApplication sharedApplication] scheduledLocalNotifications];
    NSLog(@"%@",notifications);
    
    //删除指定通知
    //    [[UIApplication sharedApplication] cancelLocalNotification:notifications[0]];
    //删除所有通知
    //    [[UIApplication sharedApplication] cancelAllLocalNotifications];
    
    /*
     执行取消本地通知的场景:
     1. 已经响应过的本地通知,需要取消。
     2. 已经递交到 application 的,但在 fire 之前 确定要取消的通知,需要取消。如提醒任务的取消,或更改提醒时间(此时应该是新的一个本地通知了)
     */
}

- (void)showInfo:(NSString *)infoStr {
    
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:infoStr preferredStyle:UIAlertControllerStyleAlert];
    
    [alertController addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:NULL]];
    
    [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alertController animated:YES completion:nil];
}


@end

需要注意的是:

1. iOS 8 后需要获取权限

//iOS 8 请求用户通知权限
if ([UIApplication instancesRespondToSelector:@selector(registerUserNotificationSettings:)]) {
    UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeBadge | UIUserNotificationTypeAlert | UIUserNotificationTypeSound categories:nil];
    [application registerUserNotificationSettings:settings];
}

调用 registerUserNotificationSettings:方法后会弹出上述权限请求框,当用户选择 "好"时,会回调 application:didRegisterUserNotificationSettings:方法

2. 在 iOS 10之前有两个地方可以响应用户点击本地通知的地方(当 app 处在后台或终止状态时)

  • 在方法- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions内通过objectForKey:UIApplicationLaunchOptionsLocalNotificationKey获取UILocalNotification实例对象,如:
    UILocalNotification *notification = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];

但是 UIApplicationLaunchOptionsLocalNotificationKey 在 iOS 10 就被禁了。

[launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey]
  • iOS 8 之后添加了方法 - (void)application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier forLocalNotification:(UILocalNotification *)notification completionHandler:(void (^)())completionHandler用于响应本地通知的 notification action 动作。

3. App 在前台时,本地通知的响应方法

当App 在前台时,fire 的本地通知将被application的- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification;方法截获

4. App 处于后台挂起(suspended)状态,但未 terminated 时,点击通知启动App响应方法

- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification;方法截获

5. 本地通知对象的存储机制

  1. 当调用scheduleLocalNotification方法添加 notification 实例时,scheduleLocalNotification 方法会对 notification 对象进行 copy ,所以需要手动 release 该 notification 对象。
  2. notification 实例中的 repeatInterval通知重复间隔属性,当其为0时表示不重复,即 fire 之后就 discard 该 notification, 即不会被 copy 进scheduledLocalNotifications数组里。
  3. scheduledLocalNotifications数组最多只能有64个 notification 实例,超过会被系统忽略,该数组会持有需要重复 fired 的 notification 对象,用于下次在指定时间间隔到达时再 fire, 如果不需要重复的 notification,即 notification.repeatInterval = 0 的话,该 notification fire 之后不会被 copy 保留到这个数组里。

6. 要想看到本地通知的效果,需要的条件

  1. app 的通知权限已被打开(iOS 8)
  2. 本地通知是操作系统统一调度的,只有App状态为 suspended 或 not running 状态下(即程序处于后台或已经 terminated【又或者还没启动】)在才能收到通知通知提醒

7. 通知的声音

  1. 真机测试声音提示最保险,iOS 8.1和 iOS 9.3的模拟器测试没有声音,iOS 10.3的模拟器测试有声音(实测)
  2. 声音文件格式必须是Linear PCM,MA4(IMA/ADPCM),uLaw,alaw中的一种,而且时间必须在30秒内
  3. 声音文件必须放到main boundle 中

8. 向右滑动本地通知即解锁打开应用,锁屏状态下通知底部提示设置
下框中的 滑动打开 xxxx 显示值由notification.alertAction = @"打开应用";控制

9. 指定通知左划的按钮步骤

  1. 创建UIMutableUserNotificationCategory对象
  2. 往 category 实例中放入UIUserNotificationAction实例对象
  3. 在通知的 category 属性值传入对应的 UIMutableUserNotificationCategory 实例的 identifier 标识值

效果:

锁屏状态左划效果 下拉效果
锁屏状态左划效果
app在后台但未锁屏,顶部弹出通知,下拉效果
在通知面板,左划效果
在通知面板,左划效果
  1. 这些动作的响应方法如下:
  2. 该方法会在启动程序后,在后台被调用
  3. 按照 id 处理完自定义响应按钮方法后立即调用 completionHandler block
  4. 如果调用 completionHandler block 失败的话,程序会立即 terminate
//iOS 8 ~ 9 ,当点击本地通知自定义的响应按钮(action btn)时,会启动 App 同时下述方法会在后台被执行
-(void)application:(UIApplication *)application 
          handleActionWithIdentifier:(NSString *)identifier 
                forLocalNotification:(UILocalNotification *)notification 
                    withResponseInfo:(NSDictionary *)responseInfo 
                   completionHandler:(void (^)())completionHandler;

10. 实测上述代码运行在 iOS 10+ 的模拟器是可以有提示声音发出的,前提是代码的 iOS Deployment Target 选择 8.0

11. iOS 8 和 iOS 9下,对应的 action 响应方法有如下,分别是 localNotification 和 remoteNotification,它们之间还有一个参数 response 的区别,调用的区别则为,iOS 8 会调用没有 response的方法,而 iOS 9 则会调用带有 response 的方法,如果在 iOS 9 上两个方法都写上的话,则只会调用iOS 9 推荐的带有 response 的方法,如果 iOS 9 如果没有写带有 response 的方法的话,默认还是会调用 iOS 8的方法


这里iOS 8 和 iOS 9 方法只有一个参数的区别,response 参数的描述是The data dictionary sent by the action.也就是用于接收 iOS 9 下 action 的parameters 属性值。

ps:在 iOS 10之前 (iOS 4 ~ 9)使用本地通知的学习就差不多了,下面我们说说 iOS 10 怎样使用 本地通知♪(*)啦啦。。。(待续~)
上述问题注意点 在 iOS 10之前 (iOS 4 ~ 9)使用本地通知 Test Demo

简单场景模拟

应用场景:一般的工具型APP会包含多个本地通知,分别设置不同的fireDate。如3天,7天,一个月分别推送一次,以唤醒用户。若一个月之内打开APP,则所有本地通知重置。
//模拟需求:这里以设置1Min和3Min的本地通知为例
Demo


2. 在 iOS 10 下使用本地通知

待续~~

三、应用通知设置界面

iOS 8 iOS 9
iOS8-NotificationSetting.gif
iOS9-NotificationSetting.gif
Show in Notification Center是可设置显示个数 Show in Notification Center只能设置是否显示

iOS 10之后就没有了 alert 样式。

iOS 10 iOS 11
iOS 10
待补充

Ref

UILocalNotification
Getting the User’s Attention While in the Background

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 极光推送: 1.JPush当前版本是1.8.2,其SDK的开发除了正常的功能完善和扩展外也紧随苹果官方的步伐,SD...
    Isspace阅读 6,597评论 10 16
  • 推送通知注意:这里说的推送通知跟NSNotification有所区别NSNotification是抽象的,不可见的...
    醉叶惜秋阅读 1,468评论 0 3
  • 概述 在多数移动应用中任何时候都只能有一个应用程序处于活跃状态,如果其他应用此刻发生了一些用户感兴趣的那么通过通知...
    莫离_焱阅读 6,387评论 1 8
  • 本地通知:就是指不需要互联网就能发出的推送通知(不需要服务器去支持),使用的场景一般是定时提醒用户完成一些任务,例...
    JaXz阅读 1,433评论 0 3
  • 安琪走的时候话都没说,她就如来的那天一样很安静。 我在睡的时候,她悄无声息的从我窗口跳了下去,醒来时窗台上的向日葵...
    五柳家先生阅读 309评论 0 2