iOS 原生推送+PushKit+本地推送(iOS8-iOS12)

iOS推送原生实现方法,本文主要实现iOS端iOS8+系统的代码内容,另外还有iOS10之后新的框架pushKit使用方法。

注意

  • 常规推送证书和pushKit推送证书是一样的;
  • 他们后台推送方法是一样的,接口通用,代码通用;
  • 但是生成的token不同,手机端接收一个是有界面提示,一个是后台提示;
  • 项目中可以混用,或用pushKit+本地通知,实现同样效果;
  • 特别强调:这些证书是后台使用,给前台xcode,打包,上线,调试证书没有半毛钱关系,xcode9后可以用自动管理,什么都不需要动,不懂的自行搜索。

1.证书创建

  • 传统推送证书:Apple Push Notification service SSL (Sandbox & Production)(自行搜索)
  • pushKit证书:VoIP Services Certificate(只有一个生产证书,自行百度)
  • 证书创建好,下载安装到钥匙串里面,(这些证书是后台使用,给前台xcode,打包,上线,调试证书没有半毛钱关系)

2.证书导出p12文件,或pem。

  • 由于两个证书操作方式一模一样,使用介绍一个就可以;
  • 导出p12,Java后台使用,或导出两个p12(公钥,密钥,导出pem PHP后台使用)
  • 生成方式,请自行搜索,下面介绍pem证书生成方式
//在桌面创建一个临时文件夹
cd ~/Desktop;
mkdir Temp_Pem;
cd ~/Desktop/Temp_Pem

//cert.p12  key.p12 是推送证书导出的 公钥 秘钥文件,记得文件copy到Temp_Pem文件夹下
openssl pkcs12 -clcerts -nokeys -out ~/Desktop/Temp_Pem/apns-dev-cert.pem -in cert.p12;
openssl pkcs12 -nocerts -out ~/Desktop/Temp_Pem/apns-dev-key.pem -in key.p12;
# 移除密码(不想移除可以不用执行这个)
# openssl rsa -in apns-dev-key.pem -out apns-dev-key-noenc.pem
# 合成最终pem文件
cat apns-dev-cert.pem apns-dev-key.pem > apns-dev.pem

3.后台PHP推送代码

本文介绍只使用PHP代码推送,我封装一个推送类,参数是token,当然你改成你的token就可以,剩下一个改动就是,推送证书文件名和密码,传统推送和pushKit是一样,你只需要调整推送文件即可,现在必须是推送地址tls

 array(
              "title"=>$title,
              "subtitle"=>$subtitle,
              "body"=>$message
            ),
            'sound' => 'default', #$sound = "ping1.caf";
            'badge' => 1
        );
        return self::push_apns($deviceToken,$aps);
    }
    public static function push_apns($deviceToken='',$aps = array('alert' =>'' ,'sound' => 'default','badge' =>0 ))
    {
        if (strlen($deviceToken)<=0) {
            return;
        }

        // $pass="12345678";//密码必须是证书的密码
        // $pemPath = dirname(__FILE__) .'/'.'apns_dev.pem'; 

        $pass="123456@keyi";//密码必须是证书的密码
        $pemPath = dirname(__FILE__) .'/'.'apns_dev_pushkit.pem'; 

        /* End of Configurable Items */
        $ctx = stream_context_create();
        // $ctx = stream_context_create([
        //  'ssl'=>[
        //      'verify_peer'=>false,
        //      'verify_peer_name'=>false
        //  ]
        // ]);

        // anps_dev_club是在同文件夹下的pem证书(配置证书)
        stream_context_set_option($ctx, 'ssl', 'local_cert', $pemPath);
        // assume the private key passphase was removed.(输入密码)
        stream_context_set_option($ctx, 'ssl', 'passphrase', $pass);
        // ssl://gateway.sandbox.push.apple.com:2195 这个是苹果开发测试地址
        // ssl://gateway.push.apple.com:2195 苹果发布运行地址
        // $apnsHost='tls://gateway.sandbox.push.apple.com:2195';
        $apnsHost='tls://gateway.sandbox.push.apple.com:2195';
         #好像这个用发布和调试都可以
        $fp = stream_socket_client($apnsHost, $err, $errstr, 60, STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT, $ctx);
        #发布
        // $fp = stream_socket_client($apnsHost, $err, $errstr, 60, STREAM_CLIENT_CONNECT, $ctx);

        if (!$fp) {
            // print "Failed to connect $err $errstrn";
            return;
        }
        else {
            //print "Connection OK-----";
        }

        // Construct the notification payload
        // array() php的数组和字典
        // $body['aps'] = array(
        //  'alert' => '推送测试!',#推送的消息
        //  'sound' => 'default', #$sound = "ping1.caf";
        //  'badge' => 4
        // );
        $body['aps']=$aps;
        # 把字典转化成 json字符串
        $payload = json_encode($body);
        // 这是去掉空格,什么的,因为token里面含有一些不用的符号
        $msg = chr(0) . pack("n",32) . pack('H*', str_replace(' ', '', $deviceToken)) . pack("n",strlen($payload)) . $payload;

        // print "sending message :" . $payload . "n".$msg;
        // 发生推送
        $result=fwrite($fp, $msg,strlen($msg));
        fclose($fp);
    }

}

?>

4.Xcode项目配置(Xcode9为例)

1.传统通知配置

  • 在设置中Capabilities 打开 Push NotificationsBackground Modes
  • Background Modes中打开 Remote notificationsBackground fetch

2.pushKit配置

由于xcode9之后,取消了VoIP的设置,可以手动在info.plist文件中加入,在Required background modes里面添加一项 App provides Voice over IP services

5.项目实现

我自己封装了一个单利类FanPushManager用来管理iOS8+的推送,只有3个代理需要在appdelgate里面实现,另外还能发送本地通知,封装了一些情况!

1.头文件 FanPushManager.h

#import 
#import 

#ifdef NSFoundationVersionNumber_iOS_9_x_Max
#import 
#endif

#import 


@interface FanPushManager : NSObject


+(instancetype)defaultManager;
-(void)registerNotification;//push传统推送
-(void)registerPKPush;//pushKit  VoIP
///关闭通知栏和Icon角标
-(void)cancelAllPush;


#pragma mark - 发送本地通知
+(void)fan_sendLocalNotificationBody:(NSString *)body;
//几秒后发生本地通知
+(void)fan_sendLocalNotificationBody:(NSString *)body intervalTime:(NSTimeInterval)intervalTime;
+(void)fan_sendLocalNotificationTitle:(NSString *)title body:(NSString *)body;
+(void)fan_sendLocalNotificationTitle:(NSString *)title subtitle:(NSString *)subtitle body:(NSString *)body;
+(void)fan_sendLocalNotificationTitle:(NSString *)title subtitle:(NSString *)subtitle body:(NSString *)body intervalTime:(NSTimeInterval)intervalTime;

@end

2. FanPushManager.m 含//Mark:标签的需要自己实现自己的业务逻辑

#import "FanPushManager.h"
@implementation FanPushManager

+(instancetype)defaultManager{
    static FanPushManager *manager;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager=[[FanPushManager alloc]init];
    });
    return manager;
}

-(void)registerNotification{
    if (@available(iOS 10.0, *)) {
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        //必须写代理,不然无法监听通知的接收与点击事件
        center.delegate = self;
        [center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert) completionHandler:^(BOOL granted, NSError * _Nullable error) {
            if (!error && granted) {
                //用户点击允许
                NSLog(@"iOS10注册通知成功");
                // 可以通过 getNotificationSettingsWithCompletionHandler 获取权限设置
                //之前注册推送服务,用户点击了同意还是不同意,以及用户之后又做了怎样的更改我们都无从得知,现在 apple 开放了这个 API,我们可以直接获取到用户的设定信息了。注意UNNotificationSettings是只读对象哦,不能直接修改!
                [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
                    //                NSLog(@"========%@",settings);
                    if(settings.authorizationStatus!=UNAuthorizationStatusAuthorized){
                        //用户没有授权或拒绝权限
                    }
                }];
            }else{
                //用户点击不允许
                NSLog(@"iOS10注册通知失败");
                //提示用户是否打开或设备支持授权
            }
        }];
        
        
    } else {
        // Fallback on earlier versions
        if (@available(iOS 8.0, *)) {
            //iOS8+
            UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeBadge|UIUserNotificationTypeSound|UIUserNotificationTypeAlert) categories:nil];
            [[UIApplication sharedApplication] registerUserNotificationSettings:settings];
            
        }
    }
    //iOS8+,包括iOS10+都是需要这个方法,才能获取
    [[UIApplication sharedApplication]registerForRemoteNotifications];
}
#pragma mark - PushKit  和 PKPushRegistryDelegate
-(void)registerPKPush{
    PKPushRegistry *pushRegistry=[[PKPushRegistry alloc]initWithQueue:dispatch_get_main_queue()];
    pushRegistry.delegate=self;
    pushRegistry.desiredPushTypes=[NSSet setWithObject:PKPushTypeVoIP];
}
-(void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)pushCredentials forType:(PKPushType)type{
    //获取token,这个token需要上传到服务器
    NSData *data=pushCredentials.token;
    NSString *token=[NSString stringWithFormat:@"%@",data];
    NSCharacterSet *set = [NSCharacterSet characterSetWithCharactersInString:@"<>"];
    token = [token stringByTrimmingCharactersInSet:set];
    token = [token stringByReplacingOccurrencesOfString:@" " withString:@""];
    NSLog(@"PushKit Token:%@",token);
    
    //MARK: 在这里补充自己的pushkit token上传到服务器的逻辑
}
//收到pushkit的通知时会调用这个方法,但是不会有UI上的显示
-(void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type{
    //iOS8.0-11.0
    [FanPushManager fan_sendLocalNotificationBody:@"iOS8 PushKit发送的推送!"];
    NSDictionary *dic=payload.dictionaryPayload[@"aps"];
    
    //MARK: 解析远程推送的消息,并做处理和跳转

}
//收到pushkit的通知时会调用这个方法,但是不会有UI上的显示
-(void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion{
    //iOS11.0+
    [FanPushManager fan_sendLocalNotificationBody:@"iOS11 PushKit发送的推送!"];
    
}


#pragma mark -  正常的推送代理,iOS 10++, iOS8的代理直接在APPdelegate里面
//iOS10+在前台模式下接受消息,正常推送
-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler API_AVAILABLE(ios(10.0)){
    UNNotificationContent * content = notification.request.content;//通知消息体内容  title subtitle  body
    if([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]
        ]) {
        //远程通知
        
    }else{
        //本地通知
        
    }
    //MARK:解析推送的消息,并做处理和跳转

    
    
    completionHandler(UNNotificationPresentationOptionBadge|UNNotificationPresentationOptionSound|UNNotificationPresentationOptionAlert);
    //这里是有点问题 的,如果用户关闭通知,但是角标不会消失,
    [UIApplication sharedApplication].applicationIconBadgeNumber=0;
//    if([UIApplication sharedApplication].applicationIconBadgeNumber>0){
//        [UIApplication sharedApplication].applicationIconBadgeNumber--;
//    }
    
}
//iOS10+在后台模式下打开消息(3DTouch不会触发次方法)
-(void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler API_AVAILABLE(ios(10.0)){
    UNNotificationContent * content = response.notification.request.content;//通知消息体内容  title subtitle  body
    if([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
        //打开远程通知
    }else{
        //打开本地通知
        
    }
    
    //MARK: 解析推送的消息,并做处理和跳转

    completionHandler(); // 系统要求执 这个 法
    [UIApplication sharedApplication].applicationIconBadgeNumber=0;
//    if([UIApplication sharedApplication].applicationIconBadgeNumber>0){
//        [UIApplication sharedApplication].applicationIconBadgeNumber--;
//    }

}

#pragma mark - 关闭通知
-(void)cancelAllPush{
    [UIApplication sharedApplication].applicationIconBadgeNumber=0;
    if (@available(iOS 10.0, *)) {
        [[UNUserNotificationCenter currentNotificationCenter] removeAllPendingNotificationRequests];
    }else{
        [[UIApplication sharedApplication] cancelAllLocalNotifications];
    }
}

/**

#pragma mark - iOS8-iOS9的推送相关
//iOS8.0-10.0
-(void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings{
    //    [[UIApplication sharedApplication]registerForRemoteNotifications];
    NSLog(@"%s",__func__);
}
-(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{
    //获取token,这个token需要上传到服务器
    NSString *token=[NSString stringWithFormat:@"%@",deviceToken];
    NSCharacterSet *set = [NSCharacterSet characterSetWithCharactersInString:@"<>"];
    token = [token stringByTrimmingCharactersInSet:set];
    token = [token stringByReplacingOccurrencesOfString:@" " withString:@""];
    NSLog(@"PushiOS10以下 Token:%@",token);
    //MARK: 上传token到服务器
}
//iOS7-iOS9之后,接收远程推送支持的方法,在前台接收,或者后台点击进入,会走此方法
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
    // Required, iOS 7 Support
    completionHandler(UIBackgroundFetchResultNewData);
    //原生推送(前台后台都进入该方法)
    [UIApplication sharedApplication].applicationIconBadgeNumber=0;
}

-(void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification{
    //接收了本地通知iOS11不设置userNotificationCenter会走也会执行这个方法
    NSLog(@"888888");
    
}
*/
#pragma mark - 发送本地通知
+(void)fan_sendLocalNotificationBody:(NSString *)body{
    [FanPushManager fan_sendLocalNotificationTitle:nil subtitle:nil body:body intervalTime:0.0f];
}
//几秒后发生本地通知
+(void)fan_sendLocalNotificationBody:(NSString *)body intervalTime:(NSTimeInterval)intervalTime{
    [FanPushManager fan_sendLocalNotificationTitle:nil subtitle:nil body:body intervalTime:intervalTime];
}
+(void)fan_sendLocalNotificationTitle:(NSString *)title body:(NSString *)body{
    [FanPushManager fan_sendLocalNotificationTitle:title subtitle:nil body:body intervalTime:0.0f];
}
+(void)fan_sendLocalNotificationTitle:(NSString *)title subtitle:(NSString *)subtitle body:(NSString *)body{
    [FanPushManager fan_sendLocalNotificationTitle:title subtitle:subtitle body:body intervalTime:0.0f];
}
+(void)fan_sendLocalNotificationTitle:(NSString *)title subtitle:(NSString *)subtitle body:(NSString *)body intervalTime:(NSTimeInterval)intervalTime{
    if (@available(iOS 10.0, *)) {
        // 设置触发条件 UNNotificationTrigger
        UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:(intervalTime?intervalTime:0.1) repeats:NO];
        
        // 创建通知内容 UNMutableNotificationContent, 注意不是 UNNotificationContent ,此对象为不可变对象。
        UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
        content.title =title;
        content.subtitle = subtitle;
        content.body = body;
        NSInteger badge=[UIApplication sharedApplication].applicationIconBadgeNumber;
        content.badge = @(badge+1);
        content.sound = [UNNotificationSound defaultSound];
//        content.userInfo = @{@"key1":@"value1",@"key2":@"value2"};//方便撤销时使用
        
        // 创建通知标示,是不是通知的标签啊
        NSString *requestIdentifier = @"Dely.X.time";
        
        // 创建通知请求 UNNotificationRequest 将触发条件和通知内容添加到请求中
        UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:requestIdentifier content:content trigger:timeTrigger];
        
        UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
        // 将通知请求 add 到 UNUserNotificationCenter
        [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
            if (!error) {
                NSLog(@"推送已添加成功 %@", requestIdentifier);
                //你自己的需求例如下面:
            }
        }];
        
    }else{
        NSDate *date = [NSDate dateWithTimeIntervalSinceNow:intervalTime];
        UILocalNotification *notification = [[UILocalNotification alloc] init];
        //设置推送时间
        notification.fireDate = date;
        //设置时区
        notification.timeZone = [NSTimeZone localTimeZone];
        //设置重复间隔
        //        notification.repeatInterval = NSWeekCalendarUnit;
        //推送声音
        notification.soundName = UILocalNotificationDefaultSoundName;
        //    notification.soundName = @"Default";
        //内容
        notification.alertBody = body;
        notification.alertTitle=title;
        //显示在icon上的红色圈中的数子
        NSInteger badge=[UIApplication sharedApplication].applicationIconBadgeNumber;
        notification.applicationIconBadgeNumber = badge+1;
        //设置userinfo 方便在之后需要撤销的时候使用
//        NSDictionary *infoDic = [NSDictionary dictionaryWithObject:@"name" forKey:@"key"];
//        notification.userInfo = infoDic;
        
        [[UIApplication sharedApplication] presentLocalNotificationNow:notification];
        //    [[UIApplication sharedApplication] scheduleLocalNotification:notification];
    }
    
}
@end

3.APPDelegate实现,引入头文件,实现3个代理

//是storyBoard创建的项目
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [[FanPushManager defaultManager]registerNotification];//如果只想用传统推送就直接注册一个
    [[FanPushManager defaultManager]registerPKPush];//pushKit框架使用
    return YES;
}

#pragma mark - iOS8-iOS9的推送相关
//iOS8.0-10.0
-(void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings{
//    [[UIApplication sharedApplication]registerForRemoteNotifications];
    NSLog(@"%s",__func__);
}
-(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{
    //获取token,这个token需要上传到服务器
    NSString *token=[NSString stringWithFormat:@"%@",deviceToken];
    NSCharacterSet *set = [NSCharacterSet characterSetWithCharactersInString:@"<>"];
    token = [token stringByTrimmingCharactersInSet:set];
    token = [token stringByReplacingOccurrencesOfString:@" " withString:@""];
    NSLog(@"Push Token:%@",token);
    //MARK: 上传token到服务器
}
//iOS7-iOS9之后,接收远程推送支持的方法,在前台接收,或者后台点击进入,会走此方法
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
    // Required, iOS 7 Support
    completionHandler(UIBackgroundFetchResultNewData);
    //原生推送(前台后台都进入该方法)
    [UIApplication sharedApplication].applicationIconBadgeNumber=0;
}

-(void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification{
    //接收了本地通知iOS11不设置userNotificationCenter会走也会执行这个方法
        NSLog(@"888888");
    
}

6.总结

后期会把含有Socket的demo和推送发到GitHub上面,欢迎小伙伴关注!
其实实现原生的推送也不是很麻烦,主要有两点问题目前不能解决,就是用户关掉通知后,脚标不消失。还有就是建议你们要么用传统推送,要么用pushKit+本地推送,实现效果也非常不错!

Like(喜欢)

有问题请直接在文章下面留言,喜欢就给个分享或者吧!

Email:[email protected]

163邮箱:[email protected]

github:https://github.com/fanxiangyang

你可能感兴趣的:(iOS 原生推送+PushKit+本地推送(iOS8-iOS12))