今天看了一篇有關iOS消息推送的文章 原文
開發環境:
** Xcode 8.3
Swift 3.1 **
隨著iOS版本的不斷提高,iOS的消息推送也越來越強大,也越容易上手了。 在iOS10中,消息推送的功能有:
- 顯示文本信息
- 播放通知聲音
- 設置 badge number
- 用戶不打開應用的情況下提供actions(下拉推送消息即可看到)
- 顯示一個媒體信息
- 在 silent 的情況讓應用在后臺執行一些任務
測試iOS APNs 的時候,需要用到的:
- 一臺iOS設備,因為在模擬器是不行的
- 一個開發者賬號(配置APNs的時候需要證書)
在本文中,使用 Pusher 扮演向iOS設備推送消息的服務器的角色,在本文的測試中你可以 直接下載Pusher
iOS的消息推送中,有三個主要步驟:
- app配置注冊APNS
- 一個Server推送消息給設備
- app接送處理APNs
1 和 3 主要是iOS開發者干的事情, 2 是消息推送服務端,國內可以用一些第三方的比如 極光推送之類的,當然公司也可以自己配制。
開始之前,下載初始化的項目 starter project, 打開運行,如圖所示:
配置App
- 打開應用,在 General 改寫 Bundler Identifier 一個唯一的標示 如: com.xxxx.yyyy
- 選擇一個開發者賬號
- 在Capabilities打開APNS:
注冊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環境下的消息推送
運行項目,可看到:
點擊 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 配置失敗,所以輸出錯誤信息查看原因就很重要了 )
編譯運行,可看到輸出一個類似于這樣的字符串:
把輸出的 Device Token 拷貝出來,放到一個地方,等一下配置的時候需要這貨
創建 SSL 證書 和 PEM 文件
在蘋果官網的開發者賬號中 步驟:Certificates, IDs & Profiles -> Identifiers -> App IDs 在該應用的 ** App IDs**中應該可以看到這樣的信息:
點擊 Edit 下拉到 Push Notifications:
在 Development SSL Certificate, 點擊 **Create Certificate… **跟隨步驟創建證書,最后下載證書,雙擊證書, 證書會添加到 Keychain
回到開發者賬號, 在應用到 **App ID ** 應用可以看到:
到這一步就OK了,你已經有了APNs 的證書了
推送消息
還記得剛才下載的** Pusher **嗎? 用那貨來發送推送消息
打開 Pusher,完成以下步驟:
- 在 Pusher 中選擇剛剛生成的證書
- 把剛剛生成的 Device Token 拷貝進去 (如果你忘了拷貝生成的 Device Token,把應用刪除,重新運行,拷貝即可)
- 修改 Pusher 內的消息體如下:
{
"aps": {
"alert": "Breaking News!",
"sound": "default",
"link_url": "https://raywenderlich.com"
}
}
- 應用推到后臺,或者鎖定
- 在 Pusher 中點擊 Push 按鈕
你的應用應該可以接受到首條推送消息:
(如果你的應用在前臺,你是收不到推送的,推到后臺,重新發送消息)
推送的一些問題
一些消息推送接收不到: 如果你同時發送多條推送,而只有一些收到,很正常!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發生一條推送消息,點擊推送消息,應該會顯示如圖:
(如果沒有接收到推送消息,可能是你的設備的 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 (把推送消息顯示在視圖內),編譯運行,保持應用在前臺或者后臺,發生推送消息,顯示如下:
好了,現在 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"
}
}
如果一切運行正常,下拉推送消息,顯示如下:
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, 顯示如下:
現在 app 已經能夠處理 action 了, 你也可以定義自己的 action 試一試。
Silent 推送消息
Silent 推送消息可以 在后臺默默的喚醒你的 app 去執行一些任務. WenderCast 可以使用它來更新 podcast list.
到 App Settings -> Capabilites 為 WenderCast 打開 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:
** Run -> Info** 選擇 Wait for executable to be launched:
在 application(_:didReceiveRemoteNotification:fetchCompletionHandler:) 方法內,打斷點看是否運行。
最后
你也可以下載 完成代碼 看運行情況,當然需要做的是修改 Bundle ID, 替換自己的證書。
雖然 APNs 對于 app 來說很重要,但是如果發生太頻繁的推送消息,用戶很可能會把 app 卸載掉,所以還是要合理發生推送消息。