iOS Apple Push Notification Service (APNs)

今天看了一篇有关iOS消息推送的文章 原文

开发环境:
** Xcode 8.3
Swift 3.1 **

随着iOS版本的不断提高,iOS的消息推送也越来越强大,也越容易上手了。 在iOS10中,消息推送的功能有:

  • 显示文本信息
  • 播放通知声音
  • 设置 badge number
  • 用户不打开应用的情况下提供actions(下拉推送消息即可看到)
  • 显示一个媒体信息
  • 在 silent 的情况让应用在后台执行一些任务

测试iOS APNs 的时候,需要用到的:

  • 一台iOS设备,因为在模拟器是不行的
  • 一个开发者账号(配置APNs的时候需要证书)

在本文中,使用 Pusher 扮演向iOS设备推送消息的服务器的角色,在本文的测试中你可以 直接下载Pusher

iOS的消息推送中,有三个主要步骤:

  1. app配置注册APNS
  2. 一个Server推送消息给设备
  3. app接送处理APNs

13 主要是iOS开发者干的事情, 2 是消息推送服务端,国内可以用一些第三方的比如 极光推送之类的,当然公司也可以自己配制。

开始之前,下载初始化的项目 starter project, 打开运行,如图所示:

initial_list-281x500.png

配置App

  • 打开应用,在 General 改写 Bundler Identifier 一个唯一的标示 如: com.xxxx.yyyy
Screen-Shot-2017-05-15-at-23.05.39-1-650x163.png
  • 选择一个开发者账号
  • Capabilities打开APNS:
screen_capabilities-480x97.png
注册APNs

AppDelegate.swift顶部导入:

import UserNotifications

添加方法:

    func resgierForPushNotifications() {
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
            print("Permision granted: \(granted)")
        }
    }

在方法 application(_:didFinishLaunchingWithOptions:):中调用 registerForPushNotifications()

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        UITabBar.appearance().barTintColor = UIColor.themeGreenColor
        UITabBar.appearance().tintColor = UIColor.white
        
        
        resgierForPushNotifications()
        
        return true
    }

** UNUserNotificationCenter**是在iOS10才又的,它的作用主要是在App内管理所有的通知活动

**requestAuthorization(options:completionHandler:) **认证APNs,指定通知类型,通知类型有:

  • .badge 在App显示消息数
  • .sound 允许App播放声音
  • ** .alert** 推送通知文本
  • .carPlay CarPlay环境下的消息推送

运行项目,可看到:

IMG_7303-281x500.png

点击 Allow 运行推送

AppDelegate:中添加方法:

    func getNotificationSettings() {
        UNUserNotificationCenter.current().getNotificationSettings { (settings) in
            print("Notification setting: \(settings)")
        }
    }

这个方法主要是查看用户允许的消息推送类型,因为用户可以拒绝消息推送,也可以在手机的设置中更改推送类型。

在 ** requestAuthorization **中调用 getNotificationSettings()

    func resgierForPushNotifications() {
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
            print("Permision granted: \(granted)")
            
            guard granted else { return }
            self.getNotificationSettings()
        }
    }

更新 getNotificationSettings()方法 如下:

    func getNotificationSettings() {
        UNUserNotificationCenter.current().getNotificationSettings { (settings) in
            print("Notification setting: \(settings)")
            
            guard settings.authorizationStatus == .authorized else { return }
            UIApplication.shared.registerForRemoteNotifications()
        }
    }

** settings.authorizationStatus == .authorized** 表明用户允许推送,** UIApplication.shared.registerForRemoteNotifications()**,实际注册APNs

添加下面两个方法,它们会被调用,显示注册结果:

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let tokenParts = deviceToken.map { data -> String in
            return String(format: "%02.2hhx", data)
        }
        
        let token = tokenParts.joined()
        print("Device Token: \(token)")
    }
    
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("Failed to register: \(error)")
    }

如果注册成功,调用: ** application(_:didRegisterForRemoteNotificationsWithDeviceToken:)**

注册失败,调用: ** application(_:didRegisterForRemoteNotificationsWithDeviceToken:)**

在 application(_:didRegisterForRemoteNotificationsWithDeviceToken:)** 方法内做的事情只是简单的把 Data 转换为 String

(一般情况如果注册失败可能是:在模拟器运行 或者 App ID 配置失败,所以输出错误信息查看原因就很重要了 )

编译运行,可看到输出一个类似于这样的字符串:

screen_device_token-480x20.png

把输出的 Device Token 拷贝出来,放到一个地方,等一下配置的时候需要这货

创建 SSL 证书 和 PEM 文件

在苹果官网的开发者账号中 步骤:Certificates, IDs & Profiles -> Identifiers -> App IDs 在该应用的 ** App IDs**中应该可以看到这样的信息:

screen_configurable_notifications-480x31.png

点击 Edit 下拉到 Push Notifications:

screen_create_cert-650x410.png

Development SSL Certificate, 点击 **Create Certificate… **跟随步骤创建证书,最后下载证书,双击证书, 证书会添加到 Keychain

screen_keychain-650x51.png

回到开发者账号, 在应用到 **App ID ** 应用可以看到:

screen_enabled-480x30.png

到这一步就OK了,你已经有了APNs 的证书了

推送消息

还记得刚才下载的** Pusher **吗? 用那货来发送推送消息
打开 Pusher,完成以下步骤:

  • Pusher 中选择刚刚生成的证书
  • 把刚刚生成的 Device Token 拷贝进去 (如果你忘了拷贝生成的 Device Token,把应用删除,重新运行,拷贝即可)
  • 修改 Pusher 内的消息体如下:
{
  "aps": {
    "alert": "Breaking News!",
    "sound": "default",
    "link_url": "https://raywenderlich.com"
  }
}
  • 应用推到后台,或者锁定
  • Pusher 中点击 Push 按钮
Screen-Shot-2017-04-30-at-13.02.25-650x312.png

你的应用应该可以接受到首条推送消息:

IMG_7304-281x500.png

(如果你的应用在前台,你是收不到推送的,推到后台,重新发送消息)

推送的一些问题

一些消息推送接收不到: 如果你同时发送多条推送,而只有一些收到,很正常!APNS 为每一个设备的App维护一个 QoS (Quality of Service) 队列. 队列的size是1,所以如果你同时发送多条推送,最后一条推送是会被覆盖的

连接 Push Notification Service 有问题: 一种情况是你用的 ports 被防火墙墙了,另一种情况可能是你的APNs证书有错误

基本推送消息的结构

更新中...

在目前的推送测试中,消息体是这样的:

{
  "aps": {
    "alert": "Breaking News!",
    "sound": "default",
    "link_url": "https://raywenderlich.com"
  }
}

下面来分析一下

"aps" 这个key 的value中,可以添加 6 个key

  • alert. 可以是字符串,亦可以是字典(如果是字典,你可以本地化文本或者修改一下通知的其它内容)
  • badge. 通知数目
  • thread-id. 整合多个通知
  • sound. 通知的声音,可以是默认的,也可以是自定义的,自定义需要少于30秒和一些小限制
  • content-availabel. 设置value 为 1 的时候, 该消息会成为 silent 的模式。下午会提及
  • category. 主要和 custom actions 相关,下午会有介绍

记得 payload 最大的为 4096 比特

处理推送消息

处理推送过来的消息(使用 actions 或者 直接点击消息 )

当你接收了一个推送消息的时候会发生什么

当接收到推送消息的时候, UIApplicationDelegate 内的代理方法会被调用,调用的情况取决于目前 app 的状态

  • 如果 app 没运行,用户点击了推送消息,则推送消息会传递给 ** application(_:didFinishLaunchingWithOptions:).** 方法
  • 如果 app 在前台或者后台则 ** application(_:didReceiveRemoteNotification:fetchCompletionHandler:) ** 方法被调用。如果用户通过点击推送消息的方式打开 app , 则 该方法可能会被再次调用,所以你可以依次更新一些UI信息或数据

处理 app 没运行的消息推送情况

在** application(_:didFinishLaunchingWithOptions:).** 方法的 return 返回前加入:

        if let notification = launchOptions?[.remoteNotification] as? [String: AnyObject] {
            if let aps = notification["aps"] as? [String: AnyObject] {
                _ = NewsItem.makeNewsItem(aps)
                (window?.rootViewController as? UITabBarController)?.selectedIndex = 1
            }
        }

它会检测 ** UIApplicationLaunchOptionsKey.remoteNotification.** 是否存在于 ** launchOptions **,编译运行,把应用结束运行关掉。用 Pusher发生一条推送消息,点击推送消息,应该会显示如图:

IMG_7306-281x500.png

(如果没有接收到推送消息,可能是你的设备的 device token 改变了,在未安装 app 或者重新安装 app 的情况下,device token 会改变)

处理 app 运行在前台或者后台的消息推送

application(_:didReceiveRemoteNotification:fetchCompletionHandler:) 方法 更新如下

func application(
  _ application: UIApplication,
  didReceiveRemoteNotification userInfo: [AnyHashable : Any],
  fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
  
  let aps = userInfo["aps"] as! [String: AnyObject]
  _ = NewsItem.makeNewsItem(aps)
}

方法的做的主要是把推送消息直接加入 NewsItem (把推送消息显示在视图内),编译运行,保持应用在前台或者后台,发生推送消息,显示如下:

IMG_7308-281x500.png

好了,现在 app 能给接收推送消息了!

为 “推送通知“ 添加 Actions

为 “推送通知“ 添加 Actions 可以为消息添加一些自定义的按钮。actions 的添加通过在 app 内为通知注册 ** categories** ,每一个 category 可以有自己的 action。一旦注册,推送的服务端可以设置消息的 category。

在示例中,会定义一个名为 ** News** category 和一个相对应的名为 View 的 action,这个 action 会让用户选择这个 action 后直接打开文章

AppDelegate 内,替换 **registerForPushNotifications() ** 如下

func registerForPushNotifications() {
  UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {
    (granted, error) in      
    print("Permission granted: \(granted)")
    
    guard granted else { return }
    
    // 1
    let viewAction = UNNotificationAction(identifier: viewActionIdentifier,
                                          title: "View",
                                          options: [.foreground])
    
    // 2
    let newsCategory = UNNotificationCategory(identifier: newsCategoryIdentifier,
                                              actions: [viewAction],
                                              intentIdentifiers: [],
                                              options: [])
    // 3
    UNUserNotificationCenter.current().setNotificationCategories([newsCategory])
    
    self.getNotificationSettings()
  }
}

代码干的事为:

// 1. 创建一个新的 notification action, 按钮标题为 View , 当触发时,app 在 foreground(前台)打开. 这个 ation 有一个 identifier, 这个 identifier 是用来标示同一个 category 内的不同 action 的

// 2. 定义一个新的 category . 包含刚刚创建的 action, 有一个自己的 identifier

// 3. 通过 ** setNotificationCategories(_:) ** 方法注册 category.

编译运行 app。

替换 Pusher 内的推送消息如下:

{
  "aps": {
    "alert": "Breaking News!",
    "sound": "default",
    "link_url": "https://raywenderlich.com",
    "category": "NEWS_CATEGORY"
  }
}

如果一切运行正常,下拉推送消息,显示如下:


IMG_7309-281x500.png

nice, 点击 View, 打开 app,但是什么都没有发生,你还需要实现一些代理方法来处理 action

处理通知的 Actions

当 actions 被触发的时候, UNUserNotificationCenter 会通知它的 delegate。在 AppDelegate.swift 内添加如下 extension

extension AppDelegate: UNUserNotificationCenterDelegate {
  
  func userNotificationCenter(_ center: UNUserNotificationCenter,
                              didReceive response: UNNotificationResponse,
                              withCompletionHandler completionHandler: @escaping () -> Void) {
    // 1
    let userInfo = response.notification.request.content.userInfo
    let aps = userInfo["aps"] as! [String: AnyObject]
    
    // 2
    if let newsItem = NewsItem.makeNewsItem(aps) {
      (window?.rootViewController as? UITabBarController)?.selectedIndex = 1
      
      // 3
      if response.actionIdentifier == viewActionIdentifier,
        let url = URL(string: newsItem.link) {
        let safari = SFSafariViewController(url: url)
        window?.rootViewController?.present(safari, animated: true, completion: nil)
      }
    }
    
    // 4
    completionHandler()
  }
}

方法代码主要做的是判断 action 的 identifier, 打开推送过来的 url。

application(_:didFinishLaunchingWithOptions:): 方法内,设置 ** UNUserNotificationCenter** 的代理

UNUserNotificationCenter.current().delegate = self

编译运行,关掉 app, 替换推送消息如下:

{
  "aps": {
    "alert": "New Posts!",
    "sound": "default",
    "link_url": "https://raywenderlich.com",
    "category": "NEWS_CATEGORY"
  }
}

下拉推送消息,点击 View action, 显示如下:

IMG_7310-281x500.png

现在 app 已经能够处理 action 了, 你也可以定义自己的 action 试一试。

Silent 推送消息

Silent 推送消息可以 在后台默默的唤醒你的 app 去执行一些任务. WenderCast 可以使用它来更新 podcast list.

App Settings -> CapabilitesWenderCast 打开 Background Modes . 勾选 Remote Notifications

现在 app 在接收到这类消息的时候就会在后台唤醒。

在 ** AppDelegate** 内,替换 ** application(_:didReceiveRemoteNotification:) ** 如下:

func application(
  _ application: UIApplication,
  didReceiveRemoteNotification userInfo: [AnyHashable : Any],
  fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
  
  let aps = userInfo["aps"] as! [String: AnyObject]
  
  // 1
  if aps["content-available"] as? Int == 1 {
    let podcastStore = PodcastStore.sharedStore
    // Refresh Podcast
    // 2
    podcastStore.refreshItems { didLoadNewItems in
      // 3
      completionHandler(didLoadNewItems ? .newData : .noData)
    }
  } else  {
    // News
    // 4
    _ = NewsItem.makeNewsItem(aps)
    completionHandler(.newData)
  }
}

代码干的事为:

// 1 判断 content-available 是否为 1 来确定是否为 Silent 通知
// 2 异步更新 podcast list
// 3 当更新完以后,调用 completionHandler 来让系统确定是否有新数据载入了
// 4 如果不是 silent 通知,假定为普通消息推送

确定调用 completionHandler 的时候传入真实的数据,系统会依次判断电池在后台运行的消耗情况,系统会在需要的时候可能会把你的 app 杀掉。

替换 Pusher 如下:

{
  "aps": {
    "content-available": 1
  }
}

如果一切正常,你是看不到什么的,当然你也可以直接加入一些print方法,查看控制台输出情况 看是否执行了, 比如替换如下:

    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        let aps = userInfo["aps"] as! [String: AnyObject]
        
        if aps["content-available"] as? Int == 1 {
            print("=== content-available")
            let podcastStore = PodcastStore.sharedStore
            podcastStore.refreshItems({ (didLoadNewItems) in
                completionHandler(didLoadNewItems ? .newData : .noData)
            })
        } else {
            print("=== no, content-availabel")
            
            _ = NewsItem.makeNewsItem(aps)
            
            completionHandler(.newData)
        }
    }

原文查看是否运行的方法是:

打开scheme:

screen_editscheme-480x191.png

** Run -> Info** 选择 Wait for executable to be launched:

screen_scheme-480x288.png

application(_:didReceiveRemoteNotification:fetchCompletionHandler:) 方法内,打断点看是否运行。

最后

你也可以下载 完成代码 看运行情况,当然需要做的是修改 Bundle ID, 替换自己的证书。

虽然 APNs 对于 app 来说很重要,但是如果发生太频繁的推送消息,用户很可能会把 app 卸载掉,所以还是要合理发生推送消息。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容

  • 概述 在多数移动应用中任何时候都只能有一个应用程序处于活跃状态,如果其他应用此刻发生了一些用户感兴趣的那么通过通知...
    莫离_焱阅读 6,506评论 1 8
  • 极光推送: 1.JPush当前版本是1.8.2,其SDK的开发除了正常的功能完善和扩展外也紧随苹果官方的步伐,SD...
    Isspace阅读 6,715评论 10 16
  • 周伟涛,现数人科技(主要产品数人云,基于 Mesos 和 Docker 技术的云操作系统)云平台负责人,曾就职于国...
    优云数智阅读 4,779评论 0 14
  • #玩卡不卡·每日一抽# 每一位都可以通过这张卡片觉察自己: 1、直觉他叫什么名字?小可怜 2、他几岁了? 3 3、...
    燕燕584阅读 202评论 0 2
  • 杨痴草 在希望的田野上 种上失望的庄稼 浇水 锄草 施肥 不打农药 不关心 天气 长势 和收成 有一天 苞谷 会长...
    杨痴草阅读 183评论 1 1