ios~ APNs (Apple 远程通知服务)

注意事项:
  1. 需要打开通知权限,
  2. 推送证书、设置info.plist文件,设置自动生成的 .entitlements文件
    【本地和远程通知编程指南】:
    apple api文档: 设置远程通知服务器

他人的demo:github
APNs (Apple远程推送通知服务)远程通知
本地通知、远程通知
本地通知和远程通知 (使用APNs).

代码:

//
//  AppDelegate.m
//

#import "AppDelegate.h"
#import <UserNotifications/UserNotifications.h>

@interface AppDelegate ()<UNUserNotificationCenterDelegate>

@end

@implementation AppDelegate
// 远程推送APNS优点:长连接、离线状态也可以、安装identifier分组通知

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    // 检查网络
    [self checkNetword];

    //注册远程推送服务
    //1.enable the Push Notifications capability in your Xcode project
    
    CGFloat version = [[[UIDevice currentDevice] systemVersion] floatValue];
    
    //设置通知类型
    if (version >= 10.0)
        {
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        center.delegate = self;
        /// 在设备可以接受任何通知之前,调用请求授权会产生提示(是否打开通知,“允许”、“不允许”):requestAuthorization
        [center requestAuthorizationWithOptions:UNAuthorizationOptionCarPlay | UNAuthorizationOptionSound | UNAuthorizationOptionBadge | UNAuthorizationOptionAlert completionHandler:^(BOOL granted, NSError * _Nullable error) {
            
            if (granted) {
                NSLog(@" iOS 10 request notification success");
            }else{
                NSLog(@" iOS 10 request notification fail");
            }
        }];
        }
    else if (version >= 8.0)
        {
        UIUserNotificationSettings *setting = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeSound | UIUserNotificationTypeBadge | UIUserNotificationTypeAlert categories:nil];
        [application registerUserNotificationSettings:setting];
        }else
            {     //iOS <= 7.0
                UIRemoteNotificationType type = UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound;
                [application registerForRemoteNotificationTypes:type];
            }
    
    //2.注册app 适用于iOS 8+
    [[UIApplication sharedApplication] registerForRemoteNotifications];
    // APNs内容获取,如果apn有值,则是通过远程通知点击进来
    NSDictionary *apn = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
    
    return YES;
}

// 新打开/后台到前台/挂起到运行 均调用  (挂起比如来电/双击home)
- (void)applicationDidBecomeActive:(UIApplication *)application {
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.

    // App icon上的徽章清零 (APNs)
    [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];

}

/// 2. 注册成功调用的方法 (远程推送通知):成功获取deviceToken,你需要将令牌deviceToken发送到后端推送服务器,这样你就可以发送通知到此设备上了
-(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
    // token 不要本地缓存,当你重新启动、用户换个手机、升级系统都会返回新的token
    // 安全的加密上传到我们的服务器❗️❗️❗️
    NSString *deviceTokenStr = [self deviceTokenStrWithDeviceToken:deviceToken];
    
    NSLog(@"注册远程通知 成功 deviceToken:%@, deviceTokenStr:%@", deviceToken, deviceTokenStr);

    if (deviceToken) {
        // (将器转换成字符串,发送后台)
        /// 为了实现这一点,可以将数组分离到其组件中,再将这些组件转换为十六进制字符串,然后将它们重新连接到字符串
        NSMutableString *deviceTokenString = [NSMutableString string];
        const char *bytes = deviceToken.bytes;
        NSInteger count = deviceToken.length;
        for (int i = 0; i < count; i++) {
            [deviceTokenString appendFormat:@"%02x", bytes[i]&0x000000FF];
        }

        //NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
       // [defaults setObject: deviceTokenString forKey: @"phoneToken"];
       //[defaults synchronize];

        //上传远程通知deviceToken到我们的后台服务器
        
    }
}

///2. 注册失败调用的方法 (远程推送通知):未获得为设备提供的令牌deviceToken时调用该方法,并显示为什么注册失败
-(void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
    //失败之后,找个合适机会再试一次
    NSLog(@"注册远程通知 失败 error:%@", error);
    
}
#pragma mark - app收到通知调用的方法
#pragma mark - ios 10+  “后台通知:在APP未运行的情况下,也可以保持及时更新”
// ios 10+ : Asks the delegate how to handle a notification that arrived while the app was running in the foreground.
/// 仅当App在前台运行时:此时用户正在使用 App,收到推送消息时默认不会弹出消息提示框,  准备呈现通知时, 才会调用该委托方法.
/// 一般在此方法里选择将通知显示为声音, 徽章, 横幅, 或显示在通知列表中.
/// @param center 用户通知中心
/// @param notification 当前通知
/// @param completionHandler 回调通知选项: 横幅, 声音, 徽章...
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {

    UNNotificationRequest *request = notification.request;
    UNNotificationContent *conten = request.content;
    NSDictionary *userInfo = conten.userInfo;

    if ([request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
        NSLog(@"即将展示远程通知");
    }else {
        NSLog(@"即将展示本地通知");
    }
    NSLog(@"title:%@, subtitle:%@, body:%@, categoryIdentifier:%@, sound:%@, badge:%@, userInfo:%@", conten.title, conten.subtitle, conten.body, conten.categoryIdentifier, conten.sound, conten.badge, userInfo);
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:[[NSString stringWithFormat:@"%@", conten.badge] integerValue]];
    // 以下是在App前台运行时, 仍要显示的通知选项
    completionHandler(UNNotificationPresentationOptionAlert + UNNotificationPresentationOptionSound + UNNotificationPresentationOptionBadge);

}

// ios 10+ : 用户点击远程通知启动app,此时用户点击推送消息会将 App 从后台唤醒,(后台进入)“提醒通知”
/// 当用户通过点击通知打开App/关闭通知或点击通知按钮时, 调用该方法.
/// (必须在application:didFinishLaunchingWithOptions:里设置代理)
/// @param center 用户通知中心
/// @param response 响应事件
/// @param completionHandler 处理完成的回调
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
{
    UNNotificationRequest *request = response.notification.request;
    UNNotificationContent *conten = request.content;
    NSDictionary *userInfo = conten.userInfo;

    if ([request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
        NSLog(@"点击了远程通知");
    }else {
        NSLog(@"点击了本地通知");
    }
    NSLog(@"title:%@, subtitle:%@, body:%@, categoryIdentifier:%@, sound:%@, badge:%@, userInfo:%@, actionIdentifier:%@", conten.title, conten.subtitle, conten.body, conten.categoryIdentifier, conten.sound, conten.badge, userInfo, response.actionIdentifier);
  
    // Always call the completion handler when done。
    // 无论是不是预期数据,都要在最后调用completionHandler(),告诉系统,你已经完成了通知打开处理
    completionHandler();
    
}

#pragma mark - ios 7,8,9
// 基于iOS7及以上的系统版本,如果是使用 iOS 7 的 Remote Notification 特性那么此函数将被调用
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    
    NSLog(@"%@",userInfo);
    
}

// 将deviceToken转换成字符串
- (NSString *)deviceTokenStrWithDeviceToken:(NSData *)deviceToken {

    NSString *tokenStr;
    
    if (deviceToken) {
        if ([[deviceToken description] containsString:@"length = "]) {  // iOS 13 DeviceToken 适配。
            NSMutableString *deviceTokenString = [NSMutableString string];
            const char *bytes = deviceToken.bytes;
            NSInteger count = deviceToken.length;
            for (int i = 0; i < count; i++) {
                [deviceTokenString appendFormat:@"%02x", bytes[i]&0x000000FF];
            }
            tokenStr = [NSString stringWithString:deviceTokenString];
        }else {
            tokenStr = [[[[deviceToken description]stringByReplacingOccurrencesOfString:@"<" withString:@""]stringByReplacingOccurrencesOfString:@">" withString:@""]stringByReplacingOccurrencesOfString:@" " withString:@""];
        }
    }
    
    return tokenStr;
}

// 检查联网状态 (为了使国行手机在第一次运行App时弹出网络权限弹框, 故需要请求网络连接)
- (void)checkNetword {
    
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.baidu.com"] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:3];
    NSURLSession *session = [NSURLSession sharedSession];
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
    [task resume];
}
/**
 
 3.上传payload和device token到APNS
 
 (1、服务器有两种方式建立和apns的安全连接(token和证书)
 (2、服务器发送POST请求:必须包含以下信息
 (3、证书建立连接的话:证书和CSR文件绑定,CSR文件作为私钥加密证书,证书当做公钥用来和APNS交互。我们服务器安装这两种证书,证书有效期1年。
 
 
 The JSON payload that you want to send
 
 The device token for the user’s device
 
 Request-header fields specifying how to deliver the notification
 
 For token-based authentication, your provider server’s current authentication token(大多是证书)
 
 HEADERS
 - END_STREAM
 + END_HEADERS
 :method = POST
 :scheme = https
 :path = /3/device/00fc13adff785122b4ad28809a3420982341241421348097878e577c991de8f0
 host = api.sandbox.push.apple.com
 apns-id = eabeae54-14a8-11e5-b60b-1697f925ec7b
 apns-push-type = alert
 apns-expiration = 0
 apns-priority = 10
 DATA
 + END_STREAM
 { "aps" : { "alert" : "Hello" } }
 
 
 
 */

/**
 4.创造一个新的远程通知
 大小限制在4~5KB之间
 json payload:aps字段告诉怎么显示,是弹框、声音或者badge
 可以自定义key,和aps字典同级
 {
 “aps” : {
     “alert” : {
         “title” : “Game Request”,
         “subtitle” : “Five Card Draw”
         “body” : “Bob wants to play poker”,
     },
     "badge" : 9,
     "sound" : "bingbong.aiff"
     “category” : “GAME_INVITATION”
 },
 “gameID” : “12345678”
 }
 
 
 */

#pragma mark - UISceneSession lifecycle

- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options  API_AVAILABLE(ios(13.0)){
    // Called when a new scene session is being created.
    // Use this method to select a configuration to create the new scene with.
    return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
}


- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions  API_AVAILABLE(ios(13.0)){
    // Called when the user discards a scene session.
    // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
    // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}



@end
  • 本地通知:

代码:

-(void)HandleTimerName
{
    NSMutableArray *arr = [[NSMutableArray alloc] init];
    for (int i =0; i<1; i++) {
        NSDictionary *dic=@{@"site_name":@"您有单词学习计划,请开始学习吧~",
                                   @"timer":self.btnRemindTime.titleLabel.text,
                                   @"infoKey":self.bookUuid,
                                @"type":@"-1",
                            @"plan_name":self.nameTextField.text
                            
                                   };
        [arr addObject:dic];
    }
    [self deleteLocalNotification:[arr objectAtIndex:0]];
    [self addLocalNotification:arr];
}

- (void)addLocalNotification:(NSArray *)array
{
    // 设置一个按照固定时间的本地推送
    NSDate *now = [NSDate date];
    NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
    NSDateComponents *components = [[NSDateComponents alloc] init];
    NSInteger unitFlags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitWeekday | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
    components = [calendar components:unitFlags fromDate:now];
    
    //   通过循环  将每一个时间都设置成本地推送
    for (int i=0; i<array.count; i++) {
        //设置时区(跟随手机的时区)
        UILocalNotification *localNotification = [[UILocalNotification alloc] init];
        localNotification.timeZone = [NSTimeZone defaultTimeZone];
        if (localNotification) {
            //   设置推送时的显示内容
            localNotification.alertBody = array[i][@"site_name"];
            localNotification.alertAction = NSLocalizedString(@"All_open",@"");
            //   推送的铃声  不能超过30秒  否则会自定变为默认铃声
            localNotification.soundName = @"2.caf";
            //小图标数字
            localNotification.applicationIconBadgeNumber++;
            NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
            [formatter setDateFormat:@"HH:mm"];
            NSDate *date = [formatter dateFromString:array[i][@"timer"]];
            //通知发出的时间
            localNotification.fireDate = date;
        }
        //循环通知的周期   每天
        localNotification.repeatInterval = kCFCalendarUnitDay;
        
        //设置userinfo方便撤销
        NSDictionary * info = @{@"plan_name":array[i][@"plan_name"],@"type":array[i][@"type"],@"infoKey":array[i][@"infoKey"],@"site_name":array[i][@"site_name"]};
        localNotification.userInfo = info;
        //启动任务
        [[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
    }
}

-(void) deleteLocalNotification:(NSDictionary *)dict
{
    // 获取所有本地通知数组
    NSArray *localNotifications = [UIApplication sharedApplication].scheduledLocalNotifications;
    for (UILocalNotification *notification in localNotifications)
    {
        NSDictionary *userInfo = notification.userInfo;
        if ([dict[@"infoKey"] isEqualToString:userInfo[@"infoKey"]]) {
            [[UIApplication sharedApplication] cancelLocalNotification:notification];
        }
    }
}

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

推荐阅读更多精彩内容