今天看了一篇有關(guān)iOS消息推送的文章 原文
開發(fā)環(huán)境:
** Xcode 8.3
Swift 3.1 **
隨著iOS版本的不斷提高脱货,iOS的消息推送也越來越強大蹭劈,也越容易上手了。 在iOS10中缓淹,消息推送的功能有:
- 顯示文本信息
- 播放通知聲音
- 設(shè)置 badge number
- 用戶不打開應(yīng)用的情況下提供actions(下拉推送消息即可看到)
- 顯示一個媒體信息
- 在 silent 的情況讓應(yīng)用在后臺執(zhí)行一些任務(wù)
測試iOS APNs 的時候磁滚,需要用到的:
- 一臺iOS設(shè)備,因為在模擬器是不行的
- 一個開發(fā)者賬號(配置APNs的時候需要證書)
在本文中,使用 Pusher 扮演向iOS設(shè)備推送消息的服務(wù)器的角色氛改,在本文的測試中你可以 直接下載Pusher
iOS的消息推送中胜卤,有三個主要步驟:
- app配置注冊APNS
- 一個Server推送消息給設(shè)備
- app接送處理APNs
1 和 3 主要是iOS開發(fā)者干的事情葛躏, 2 是消息推送服務(wù)端舰攒,國內(nèi)可以用一些第三方的比如 極光推送之類的芒率,當然公司也可以自己配制偶芍。
開始之前匪蟀,下載初始化的項目 starter project, 打開運行材彪,如圖所示:
配置App
- 打開應(yīng)用,在 General 改寫 Bundler Identifier 一個唯一的標示 如: com.xxxx.yyyy
- 選擇一個開發(fā)者賬號
- 在Capabilities打開APNS:
注冊APNs
在 AppDelegate.swift頂部導(dǎo)入:
import UserNotifications
添加方法:
func resgierForPushNotifications() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
print("Permision granted: \(granted)")
}
}
在方法 application(_:didFinishLaunchingWithOptions:):中調(diào)用 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內(nèi)管理所有的通知活動
**requestAuthorization(options:completionHandler:) **認證APNs,指定通知類型港谊,通知類型有:
- .badge 在App顯示消息數(shù)
- .sound 允許App播放聲音
- ** .alert** 推送通知文本
- .carPlay CarPlay環(huán)境下的消息推送
運行項目歧寺,可看到:
點擊 Allow 運行推送
在 AppDelegate:中添加方法:
func getNotificationSettings() {
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
print("Notification setting: \(settings)")
}
}
這個方法主要是查看用戶允許的消息推送類型,因為用戶可以拒絕消息推送奴艾,也可以在手機的設(shè)置中更改推送類型蕴潦。
在 ** requestAuthorization **中調(diào)用 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
添加下面兩個方法蝗碎,它們會被調(diào)用蹦骑,顯示注冊結(jié)果:
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)")
}
如果注冊成功,調(diào)用: ** application(_:didRegisterForRemoteNotificationsWithDeviceToken:)**
注冊失敗袱衷,調(diào)用: ** application(_:didRegisterForRemoteNotificationsWithDeviceToken:)**
在 application(_:didRegisterForRemoteNotificationsWithDeviceToken:)** 方法內(nèi)做的事情只是簡單的把 Data 轉(zhuǎn)換為 String
(一般情況如果注冊失敗可能是:在模擬器運行 或者 App ID 配置失敗致燥,所以輸出錯誤信息查看原因就很重要了 )
編譯運行谜叹,可看到輸出一個類似于這樣的字符串:
把輸出的 Device Token 拷貝出來荷腊,放到一個地方女仰,等一下配置的時候需要這貨
創(chuàng)建 SSL 證書 和 PEM 文件
在蘋果官網(wǎng)的開發(fā)者賬號中 步驟:Certificates, IDs & Profiles -> Identifiers -> App IDs 在該應(yīng)用的 ** App IDs**中應(yīng)該可以看到這樣的信息:
點擊 Edit 下拉到 Push Notifications:
在 Development SSL Certificate, 點擊 **Create Certificate… **跟隨步驟創(chuàng)建證書床三,最后下載證書,雙擊證書, 證書會添加到 Keychain
回到開發(fā)者賬號, 在應(yīng)用到 **App ID ** 應(yīng)用可以看到:
到這一步就OK了撇簿,你已經(jīng)有了APNs 的證書了
推送消息
還記得剛才下載的** Pusher **嗎聂渊? 用那貨來發(fā)送推送消息
打開 Pusher,完成以下步驟:
- 在 Pusher 中選擇剛剛生成的證書
- 把剛剛生成的 Device Token 拷貝進去 (如果你忘了拷貝生成的 Device Token四瘫,把應(yīng)用刪除汉嗽,重新運行,拷貝即可)
- 修改 Pusher 內(nèi)的消息體如下:
{
"aps": {
"alert": "Breaking News!",
"sound": "default",
"link_url": "https://raywenderlich.com"
}
}
- 應(yīng)用推到后臺找蜜,或者鎖定
- 在 Pusher 中點擊 Push 按鈕
你的應(yīng)用應(yīng)該可以接受到首條推送消息:
(如果你的應(yīng)用在前臺,你是收不到推送的洗做,推到后臺弓叛,重新發(fā)送消息)
推送的一些問題
一些消息推送接收不到: 如果你同時發(fā)送多條推送,而只有一些收到诚纸,很正常邪码!APNS 為每一個設(shè)備的App維護一個 QoS (Quality of Service) 隊列. 隊列的size是1,所以如果你同時發(fā)送多條推送咬清,最后一條推送是會被覆蓋的
連接 Push Notification Service 有問題: 一種情況是你用的 ports 被防火墻墻了闭专,另一種情況可能是你的APNs證書有錯誤
基本推送消息的結(jié)構(gòu)
更新中...
在目前的推送測試中,消息體是這樣的:
{
"aps": {
"alert": "Breaking News!",
"sound": "default",
"link_url": "https://raywenderlich.com"
}
}
下面來分析一下
在 "aps" 這個key 的value中旧烧,可以添加 6 個key
- alert. 可以是字符串影钉,亦可以是字典(如果是字典,你可以本地化文本或者修改一下通知的其它內(nèi)容)
- badge. 通知數(shù)目
- thread-id. 整合多個通知
- sound. 通知的聲音掘剪,可以是默認的平委,也可以是自定義的,自定義需要少于30秒和一些小限制
- content-availabel. 設(shè)置value 為 1 的時候夺谁, 該消息會成為 silent 的模式廉赔。下午會提及
- category. 主要和 custom actions 相關(guān)肉微,下午會有介紹
記得 payload 最大的為 4096 比特
處理推送消息
處理推送過來的消息(使用 actions 或者 直接點擊消息 )
當你接收了一個推送消息的時候會發(fā)生什么
當接收到推送消息的時候, UIApplicationDelegate 內(nèi)的代理方法會被調(diào)用蜡塌,調(diào)用的情況取決于目前 app 的狀態(tài)
- 如果 app 沒運行碉纳,用戶點擊了推送消息,則推送消息會傳遞給 ** application(_:didFinishLaunchingWithOptions:).** 方法
- 如果 app 在前臺或者后臺則 ** application(_:didReceiveRemoteNotification:fetchCompletionHandler:) ** 方法被調(diào)用馏艾。如果用戶通過點擊推送消息的方式打開 app 劳曹, 則 該方法可能會被再次調(diào)用,所以你可以依次更新一些UI信息或數(shù)據(jù)
處理 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 **琅摩,編譯運行铁孵,把應(yīng)用結(jié)束運行關(guān)掉。用 Pusher發(fā)生一條推送消息房资,點擊推送消息蜕劝,應(yīng)該會顯示如圖:
(如果沒有接收到推送消息,可能是你的設(shè)備的 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 (把推送消息顯示在視圖內(nèi)),編譯運行溉浙,保持應(yīng)用在前臺或者后臺烫止,發(fā)生推送消息,顯示如下:
好了戳稽,現(xiàn)在 app 能給接收推送消息了馆蠕!
為 “推送通知“ 添加 Actions
為 “推送通知“ 添加 Actions 可以為消息添加一些自定義的按鈕。actions 的添加通過在 app 內(nèi)為通知注冊 ** categories** 惊奇,每一個 category 可以有自己的 action互躬。一旦注冊,推送的服務(wù)端可以設(shè)置消息的 category颂郎。
在示例中吼渡,會定義一個名為 ** News** category 和一個相對應(yīng)的名為 View 的 action,這個 action 會讓用戶選擇這個 action 后直接打開文章
在 AppDelegate 內(nèi)乓序,替換 **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. 創(chuàng)建一個新的 notification action, 按鈕標題為 View , 當觸發(fā)時寺酪,app 在 foreground(前臺)打開. 這個 ation 有一個 identifier, 這個 identifier 是用來標示同一個 category 內(nèi)的不同 action 的
// 2. 定義一個新的 category . 包含剛剛創(chuàng)建的 action替劈, 有一個自己的 identifier
// 3. 通過 ** setNotificationCategories(_:) ** 方法注冊 category.
編譯運行 app寄雀。
替換 Pusher 內(nèi)的推送消息如下:
{
"aps": {
"alert": "Breaking News!",
"sound": "default",
"link_url": "https://raywenderlich.com",
"category": "NEWS_CATEGORY"
}
}
如果一切運行正常,下拉推送消息陨献,顯示如下:
nice, 點擊 View, 打開 app盒犹,但是什么都沒有發(fā)生,你還需要實現(xiàn)一些代理方法來處理 action
處理通知的 Actions
當 actions 被觸發(fā)的時候, UNUserNotificationCenter 會通知它的 delegate急膀。在 AppDelegate.swift 內(nèi)添加如下 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:): 方法內(nèi)卓嫂,設(shè)置 ** UNUserNotificationCenter** 的代理
UNUserNotificationCenter.current().delegate = self
編譯運行慷暂,關(guān)掉 app, 替換推送消息如下:
{
"aps": {
"alert": "New Posts!",
"sound": "default",
"link_url": "https://raywenderlich.com",
"category": "NEWS_CATEGORY"
}
}
下拉推送消息命黔,點擊 View action呜呐, 顯示如下:
現(xiàn)在 app 已經(jīng)能夠處理 action 了就斤, 你也可以定義自己的 action 試一試悍募。
Silent 推送消息
Silent 推送消息可以 在后臺默默的喚醒你的 app 去執(zhí)行一些任務(wù). WenderCast 可以使用它來更新 podcast list.
到 App Settings -> Capabilites 為 WenderCast 打開 Background Modes . 勾選 Remote Notifications
現(xiàn)在 app 在接收到這類消息的時候就會在后臺喚醒。
在 ** AppDelegate** 內(nèi)洋机,替換 ** 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 當更新完以后坠宴,調(diào)用 completionHandler 來讓系統(tǒng)確定是否有新數(shù)據(jù)載入了
// 4 如果不是 silent 通知,假定為普通消息推送
確定調(diào)用 completionHandler 的時候傳入真實的數(shù)據(jù)绷旗,系統(tǒng)會依次判斷電池在后臺運行的消耗情況喜鼓,系統(tǒng)會在需要的時候可能會把你的 app 殺掉。
替換 Pusher 如下:
{
"aps": {
"content-available": 1
}
}
如果一切正常衔肢,你是看不到什么的庄岖,當然你也可以直接加入一些print方法,查看控制臺輸出情況 看是否執(zhí)行了, 比如替換如下:
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:) 方法內(nèi)角骤,打斷點看是否運行隅忿。
最后
你也可以下載 完成代碼 看運行情況,當然需要做的是修改 Bundle ID邦尊, 替換自己的證書背桐。
雖然 APNs 對于 app 來說很重要,但是如果發(fā)生太頻繁的推送消息蝉揍,用戶很可能會把 app 卸載掉链峭,所以還是要合理發(fā)生推送消息。