Remote Notification

remote Notification

在做远程推送的时候,需要向apple申请证书上的支持。首先介绍如何完成远程推送的前期准备,主要是证书的申请和制作,这里只做简单介绍。主要的操作都是在macOS系统上完成的,跟着操作一步一步来,就可以生成测试使用的证书:

  • 登录到 iPhone Developer Connection Portal 并点击 App IDs
  • 创建一个不使用通配符的 App ID 。通配符 ID 不能用于推送通知服务。例如,我们的iPhone程序ID像这样: AB123346CD.com.serverdensity.iphone
  • 点击App ID旁的“Configure”,然后按下按钮生产 推送通知许可证。根据“向导”指导的步骤生成一个签名并上传,最后下载生成的许可证。此步骤在 Apple文档中 也有谈到。
  • 通过双击.cer文件将你的 aps_developer_identity.cer 引入Keychain中。
  • 在Mac上启动 Keychain助手,然后在login keychain中选择 Certificates分类。你将看到一个可扩展选项“Apple Development Push Services”
  • 扩展此选项然后右击“Apple Development Push Services” > Export “Apple Development Push Services ID123”。保存为 apns-dev-cert.p12 文件。
  • 扩展“Apple Development Push Services” 对“Private Key”做同样操作,保存为 apns-dev-key.p12 文件。
  • 需要通过终端命令将这些文件转换为PEM格式:
openssl pkcs12 -clcerts -nokeys -out apns-dev-cert.pem -in apns-dev-cert.p12
  • 转换得到key的pem:

    openssl pkcs12 -nocerts -out apns-dev-key.pem -in apns-dev-cert.p12
  • 如果你想要移除密码,要么在导出/转换时不要设定或者执行:

    openssl rsa -in apns-dev-key.pem -out apns-dev-key-noenc.pem
  • 最后,你需要将键和许可文件合成为apns-dev.pem文件,此文件在连接到APNS时需要使用:

    cat apns-dev-cert.pem apns-dev-key-noenc.pem > apns-dev.pem  
    或者使用有密码的: 
    cat apns-dev-cert.pem apns-dev-key.pem > ck.pem

将此文件保存为一个易记的名字,你有可能以后会用到它。上述步骤同样适合于生成产品许可证。

注:以上使用的证书转换成p12证书使用的密码都是1234

检验证书是否正确的方法:

telnet gateway.sandbox.push.apple.com 2195

上面的命令执行的返回

Trying 17.172.232.226…

Connected to gateway.sandbox.push-apple.com.akadns.net.

Escape character is ‘^]’.

它将尝试发送一个规则的,不加密的连接到APNS服务。如果你看到上面的反馈,那说明你的MAC能够到达APNS。按下Ctrl C 关闭连接。如果得到一个错误信息,那么你需要确保你的防火墙允许2195端口。

然后再次连接,这次用我们的SSL证书和私钥来设置一个安全的连接:

openssl s_client -connect gateway.sandbox.push.apple.com:2195 -cert apns-dev-cert.pem -key apns-dev-key.pem

当你看到反馈的最下方returnCode=0就成功了

这里给出一个简单的pushme.php文件

<?php
    /**
    {
        "aps":{
            "alert":"此处有两个服务器需要选择,如果是开发测试用,选择第二名sandbox的服务器并使用Dev的pem证书,如果是正是发布,使用Product的pem并选用正式的服务器",     //消息首页展示内容
            "badge":10,                    //icon上未读消息标示个数
            "sound":"default",            //推送听到的铃声
            "data":{                    //推送消息主体,供程序启动时做相应处理
                "tid":1000001,            //通知Id 做设备打开通知上报数据使用
                "id":101,                //跳转id [可为课程id/计划id/文章id/ 是web页面默认为0]
                "type":0,    // 0 系统通知[默认] 1 好友新消息  2 新用户注册  3好友请求 4课程更新
                "systype":1,  //1 课程 2计划 3文章 4活动 5web页面跳转
                "url":"http://www.imooc.com/abc.html" //为4web页面跳转使用 |非4web页面清除url字段
            },
            "category"=>"CATEGORY_ID"     //用来快捷处理消息唯一标示
        }
    }

    */
    $deviceToken= 'ef92d3d412bd4c95d65b07c66170be8*******a6c25e54d643f3f3c707'; //没有空格
    $body = [];
    $type = 5;  //1 课程 2计划 3文章 4活动 5web页面跳转 控制推送内容

    ///拼接推送字符串
    switch ($type) {
        case 1:
            $content = '从搭建Golang开发环境开始, 一步步介绍Golang系统库之输入输出的功能及特性。结合行数统计及图片读取,在实战中扎扎实实的学习Golang';
            $body = array("aps" => array("alert" => $content,"badge" => 10,"sound"=>'default','data'=>array('tid'=>10000,'id'=>492,'type'=>0,'systype'=>$type)));
            break;
        case 2:
            $content = '随着互联网的发展速度迅猛,前端工程师职业越来越火热,想学习Web前端技能吗 ? 该路径从基础知识到实战案例演练,一步步带您快速掌握如何搭建网站静态页面、开发网站交互特效,为您打开WEB前端工程师大门。还在等什么?快来学习吧!';
            $body = array("aps" => array("alert" => $content,"badge" => 11,"sound"=>'default','data'=>array('tid'=>10001,'id'=>32,'type'=>0,'systype'=>$type)));
            break;
        case 3:
            $content = 'CodeStriker CodeStriker是一个免费&开源的Web应用程序,可以帮助开发人员基于Web的代码审查。它不但允许开发人员将问题、意见和决定记录在数据库中,还为实际执行代码审查提供了一个舒适的工作区域。 官方网站:http://codestriker.sourceforge.net/index.html 2)RhodeCode Rhode';
            $body = array("aps" => array("alert" => $content,"badge" => 12,"sound"=>'default','data'=>array('tid'=>10002,'id'=>2493,'type'=>0,'systype'=>$type)));
            break;
        case 4:
            $content = '高薪捉拿程序大拿';
            $body = array("aps" => array("alert" => $content,"badge" => 12,"sound"=>'default','data'=>array('tid'=>27820,'id'=>2493,'type'=>0,'systype'=>$type,'url'=>'http://t.imooc.com')));
            break;
        case 5:
            $alertBody = '我是整个alert的标题';
            $title = '我是一个主标题';
            $subtitle = '我是一个副标题';
            $body = array("aps" => array("alert" =>array("body"=>$alertBody,"title"=>$title,"subtitle"=>$subtitle),"mutable-content"=>1,"badge" => 13,"sound"=>'default','data'=>array('tid'=>10003,'id'=>0,'type'=>0,'systype'=>$type,'url'=>'http://t.imooc.com',"helloKey"=>"helloValue"),"category"=>"CATEGORY_ID"),"my-attachment"=>"http://newpic.jxnews.com.cn/003/001/209/00300120964_fab36aec.jpg");
            break;
        default:
            # code...
            break;
    }

    $ctx = stream_context_create();
    //如果在Windows的服务器上,寻找pem路径会有问题,路径修改成这样的方法:
    //$pem = dirname(__FILE__) . '/' . 'apns-dev.pem';
    //linux 的服务器直接写pem的路径即可
    stream_context_set_option($ctx,"ssl","local_cert","ck.pem");
    $pass = "1234";
    stream_context_set_option($ctx, 'ssl', 'passphrase', $pass);
    //此处有两个服务器需要选择,如果是开发测试用,选择第二名sandbox的服务器并使用Dev的pem证书,如果是正是发布,使用Product的pem并选用正式的服务器
    // $fp = stream_socket_client("ssl://gateway.push.apple.com:2195", $err, $errstr, 60, STREAM_CLIENT_CONNECT, $ctx);
    $fp = stream_socket_client("ssl://gateway.sandbox.push.apple.com:2195", $err, $errstr, 60, STREAM_CLIENT_CONNECT, $ctx);
    if (!$fp) {
    echo "Failed to connect $err $errstrn";
    return;
    }
    print "Connection OK\\n";
    $payload = json_encode($body);
    $msg = chr(0) . pack("n",32) . pack("H*", str_replace(' ', '', $deviceToken)) . pack("n",strlen($payload)) . $payload;
    echo "sending message :" . $payload ."\\n";
    fwrite($fp, $msg);
    fclose($fp);
?>

直接从网上找到的,没有改太多,但是很好用就是了。当你将ck.pem和pushme.php文件放在同一个文件夹下的时候,修改文件中的token和密码就可以发送到测试手机了。

如下是一个推送Notification打印的时候包含的所有信息

{
UNNotificationResponse: 0x17022b800; 
actionIdentifier: com.apple.UNNotificationDefaultActionIdentifier, 
notification: {
            UNNotification: 0x17022b960; date: 2016-07-25 10:06:13 +0000, 
            request: {
                    UNNotificationRequest: 0x174022a60; 
                    identifier: 802B986C-928B-40E9-8AC9-B6649DE5BC2C,
                     content: {
                            UNNotificationContent: 0x1740e9780; 
                            title: 我是一个主标题, 
                            subtitle: 我是一个副标题, 
                            body: 我是整个alert的标题, 
                            categoryIdentifier: CATEGORY_ID, 
                            launchImageName: , 
                            peopleIdentifiers: (), 
                            threadIdentifier: , 
                            attachments: (), 
                            badge: 13, 
                            sound: {
                                    UNNotificationSound: 0x1740bde20
                                      }, 
                            hasDefaultAction: YES, 
                            shouldAddToNotificationsList: YES, 
                            shouldAlwaysAlertWhileAppIsForeground: NO, 
                            shouldLockDevice: NO, 
                            shouldPauseMedia: NO, 
                            isSnoozeable: NO, 
                            fromSnooze: NO, 
                            darwinNotificationName: (null), 
                            darwinSnoozedNotificationName: (null), 
                            trigger: {
                                    UNPushNotificationTrigger: 0x170000ff0; 
                                    contentAvailable: NO, 
                                    mutableContent: YES
                                    }
                            }
        }
}

从打印信息中我们没有看到自定义的信息,是不是出了问题。打一下断点调试一下,原来userInfo没有打印出来,Why,应该被打印的信息为什么被省略了。如下图

在上面的图片中就包含了我们自定义的信息。但是这里有一个问题,就是NotificationContent对象会向UserInfo这个对象中做检索,将里面对应key的信息提取到上层中直接展示。这一点在代码处理上也得到了验证。

到目前为止,工程已经调试完毕了。但是现在想尝试一下NotificationServie是如何被触发的,这样就可以自定义相关的notification第一个界面的展示了。在此之前 你需要做如下的操作,

  • target中增加notificationServieExtension
  • 将主工程的target和extension的target中的capabilities中的push Notification通知打开。 这里需要注意的一点是。这里使用的证书必须是同一个teamID下的

测试的文件有image,gif,movie,但这些资源文件都是本地的(文件添加在extension中的bundle中)。刚才又测试了一下网络的数据传输,发现一个坑。在Apple的attachment附件api定义中附件的展示是一个url,也就是说当我拿到了一个例如网络图片的data数据的时候还是不能直接展示,需要将data存在在一个文件夹下生成对应的url,才能展示。但是extension不能操作APP的沙河,存储数据就只能写在APP group这个数据共享中,但简单的存储数据违背了app group共享数据的初衷。还有就是现在beta测试中不能同时打开push notification和App group两个capabilities,否则远程推送不能送达。

@available(iOS 10.0, *)
public class UNNotificationAttachment : NSObject, NSCopying, NSSecureCoding {


    // The identifier of this attachment
    public var identifier: String { get }


    // The URL to the attachment's data. If you have obtained this attachment from UNUserNotificationCenter then the URL will be security-scoped.
    public var url: URL { get }


    // The UTI of the attachment.
    public var type: String { get }


    // Creates an attachment for the data at URL with an optional options dictionary. URL must be a file URL. Returns nil if the data at URL is not supported.
    public convenience init(identifier: String, url URL: URL, options: [NSObject : AnyObject]? = [:]) throws
}

// Key to manually provide a type hint for the attachment. If not set the type hint will be guessed from the attachment's file extension. Value must be an NSString.
@available(iOS 10.0, *)
public let UNNotificationAttachmentOptionsTypeHintKey: String

// Key to specify if the thumbnail for this attachment is hidden. Defaults to NO. Value must be a boolean NSNumber.
@available(iOS 10.0, *)
public let UNNotificationAttachmentOptionsThumbnailHiddenKey: String

// Key to specify a normalized clipping rectangle to use for the attachment thumbnail. Value must be a CGRect encoded using CGRectCreateDictionaryRepresentation.
@available(iOS 10.0, *)
public let UNNotificationAttachmentOptionsThumbnailClippingRectKey: String

// Key to specify the animated image frame number or the movie time to use as the thumbnail.
// An animated image frame number must be an NSNumber. A movie time must either be an NSNumber with the time in seconds or a CMTime encoded using CMTimeCopyAsDictionary.
@available(iOS 10.0, *)
public let UNNotificationAttachmentOptionsThumbnailTimeKey: String

增加一个resourceData会更好,我是这样想的。这也是后话了,现在我使用的是Xcode beta 3和ios 10 beta 3。希望在之后会有改进

先让我们看一下资源文件在notification中是怎样的变现。这里根据WWDC的视屏,在target中添加了

  • image
  • gif
  • movie

三个样式的资源,让我们看一下实现。

override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler:(UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)

        if let bestAttemptContent = bestAttemptContent {
            // Modify the notification content here...
            bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
//            let path = Bundle.main.pathForResource("tempImage", ofType: "png")
//            let path = Bundle.main.pathForResource("hello", ofType: "gif")
            let path = Bundle.main.pathForResource("tempVideo", ofType: "mov")
            let pathURL = URL.init(fileURLWithPath: path!)

            do {
                let attachment = try UNNotificationAttachment(identifier: "movie",url: pathURL,options: nil)
                bestAttemptContent.attachments = [ attachment ]
            } catch {
                print("出错了")
                print(error)
            }
            contentHandler(bestAttemptContent)
        }
    }

下面看一下展示