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 Notifications
,Background Modes
- 在
Background Modes
中打开Remote notifications
,Background 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+本地推送,实现效果也非常不错!