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 卸載掉,所以還是要合理發生推送消息。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 概述 在多數移動應用中任何時候都只能有一個應用程序處于活躍狀態,如果其他應用此刻發生了一些用戶感興趣的那么通過通知...
    莫離_焱閱讀 6,550評論 1 8
  • 極光推送: 1.JPush當前版本是1.8.2,其SDK的開發除了正常的功能完善和擴展外也緊隨蘋果官方的步伐,SD...
    Isspace閱讀 6,774評論 10 16
  • 周偉濤,現數人科技(主要產品數人云,基于 Mesos 和 Docker 技術的云操作系統)云平臺負責人,曾就職于國...
    優云數智閱讀 4,811評論 0 14
  • #玩卡不卡·每日一抽# 每一位都可以通過這張卡片覺察自己: 1、直覺他叫什么名字?小可憐 2、他幾歲了? 3 3、...
    燕燕584閱讀 208評論 0 2
  • 楊癡草 在希望的田野上 種上失望的莊稼 澆水 鋤草 施肥 不打農藥 不關心 天氣 長勢 和收成 有一天 苞谷 會長...
    楊癡草閱讀 194評論 1 1