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, 打开运行,如图所示:

iOS Apple Push Notification Service (APNs)_第1张图片
initial_list-281x500.png

配置App

  • 打开应用,在 General 改写 Bundler Identifier 一个唯一的标示 如: com.xxxx.yyyy
iOS Apple Push Notification Service (APNs)_第2张图片
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环境下的消息推送

运行项目,可看到:

iOS Apple Push Notification Service (APNs)_第3张图片
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:

iOS Apple Push Notification Service (APNs)_第4张图片
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 按钮
iOS Apple Push Notification Service (APNs)_第5张图片
Screen-Shot-2017-04-30-at-13.02.25-650x312.png

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

iOS Apple Push Notification Service (APNs)_第6张图片
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发生一条推送消息,点击推送消息,应该会显示如图:

iOS Apple Push Notification Service (APNs)_第7张图片
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 (把推送消息显示在视图内),编译运行,保持应用在前台或者后台,发生推送消息,显示如下:

iOS Apple Push Notification Service (APNs)_第8张图片
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"
  }
}

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


iOS Apple Push Notification Service (APNs)_第9张图片
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, 显示如下:

iOS Apple Push Notification Service (APNs)_第10张图片
IMG_7310-281x500.png

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

Silent 推送消息

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

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

iOS Apple Push Notification Service (APNs)_第11张图片

现在 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:

iOS Apple Push Notification Service (APNs)_第12张图片
screen_editscheme-480x191.png

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

iOS Apple Push Notification Service (APNs)_第13张图片
screen_scheme-480x288.png

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

最后

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

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

你可能感兴趣的:(iOS Apple Push Notification Service (APNs))