美文网首页Flutter学习
iOS Apple Push Notification Serv

iOS Apple Push Notification Serv

作者: 阿朱先生 | 来源:发表于2017-06-20 22:41 被阅读0次

    今天看了一篇有关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 卸载掉,所以还是要合理发生推送消息。

    相关文章

      网友评论

        本文标题:iOS Apple Push Notification Serv

        本文链接:https://www.haomeiwen.com/subject/mgmaqxtx.html