PushNotification發(fā)展歷史
iOS 10 中,把以前雜亂的和通知相關(guān)的 API 都統(tǒng)一了,現(xiàn)在開(kāi)發(fā)者可以使用獨(dú)立的UserNotifications.framework
來(lái)集中管理和使用 iOS 系統(tǒng)中通知的功能护盈。在此基礎(chǔ)上勇皇,Apple 還增加了撤回單條通知,更新已展示通知化漆,中途修改通知內(nèi)容抖誉,在通知中展示圖片視頻殊轴,自定義通知 UI 等一系列新功能衰倦,非常強(qiáng)大袒炉。
對(duì)于開(kāi)發(fā)者來(lái)說(shuō),相較于之前版本樊零,iOS 10 提供了一套非常易用的通知處理接口我磁,是 SDK 的一次重大重構(gòu)。而之前的絕大部分通知相關(guān) API 都已經(jīng)被標(biāo)為棄用 (deprecated)驻襟。下面我們來(lái)回顧一下PushNotification的發(fā)展歷程夺艰。
iOS 3 - 引入推送通知:Application
的registerForRemoteNotificationTypes
與UIApplicationDelegate
的application(_:didRegisterForRemoteNotificationsWithDeviceToken:),application(_:didReceiveRemoteNotification:)
iOS 4 - 引入本地通知scheduleLocalNotification
沉衣,presentLocalNotificationNow:
郁副,application(_:didReceive:)
iOS 5 - 加入通知中心頁(yè)面
iOS 6 - 通知中心頁(yè)面與 iCloud 同步
iOS 7 - 后臺(tái)靜默推送application(_:didReceiveRemoteNotification:fetchCompletionHandle:)
iOS 8 - 重新設(shè)計(jì) Notification
權(quán)限請(qǐng)求,Actionable 通知registerUserNotificationSettings(_:)
豌习,UIUserNotificationAction
與UIUserNotificationCategory
存谎,application(_:handleActionWithIdentifier:forRemoteNotification:completionHandler:)
等
iOS 9 - Text Input action,基于 HTTP/2 的推送請(qǐng)求UIUserNotificationActionBehavior
肥隆,全新的 Provider API 等
現(xiàn)狀分析
有點(diǎn)暈既荚,不是么?一個(gè)開(kāi)發(fā)者很難在不借助于文檔的幫助下區(qū)分 application(_:didReceiveRemoteNotification:)
和application(_:didReceiveRemoteNotification:fetchCompletionHandle:)
栋艳,新入行的開(kāi)發(fā)者也不可能明白 registerForRemoteNotificationTypes
和 registerUserNotificationSettings(_:)
之間是不是有什么關(guān)系恰聘,Remote 和 Local Notification 除了在初始化方式之外那些細(xì)微的區(qū)別也讓人抓狂,而很多 API 都被隨意地放在了 UIApplication 或者 UIApplicationDelegate 中吸占。除此之外晴叨,應(yīng)用已經(jīng)在前臺(tái)時(shí),遠(yuǎn)程推送是無(wú)法直接顯示的矾屯,要先捕獲到遠(yuǎn)程來(lái)的通知篙螟,然后再發(fā)起一個(gè)本地通知才能完成顯示。更讓人郁悶的是问拘,應(yīng)用在運(yùn)行時(shí)和非運(yùn)行時(shí)捕獲通知的路徑還不一致遍略。雖然這些種種問(wèn)題都是由一定歷史原因造成的惧所,但不可否認(rèn),正是混亂的組織方式和之前版本的考慮不周绪杏,使得 iOS 通知方面的開(kāi)發(fā)一直稱不上“讓人愉悅”下愈,甚至有不少“壞代碼”的味道。
另一方面蕾久,現(xiàn)在的通知功能相對(duì)還是簡(jiǎn)單势似,我們能做的只是本地或者遠(yuǎn)程發(fā)起通知,然后顯示給用戶僧著。雖然 iOS 8 和 9 中添加了按鈕和文本來(lái)進(jìn)行交互履因,但是已發(fā)出的通知不能更新,通知的內(nèi)容也只是在發(fā)起時(shí)唯一確定盹愚,而這些內(nèi)容也只能是簡(jiǎn)單的文本栅迄。 想要在現(xiàn)有基礎(chǔ)上擴(kuò)展通知的功能,勢(shì)必會(huì)讓原本就盤(pán)根錯(cuò)節(jié)的 API 更加難以理解皆怕。
在 iOS 10 中新加入 UserNotifications 框架毅舆,可以說(shuō)是 iOS SDK 發(fā)展到現(xiàn)在的最大規(guī)模的一次重構(gòu)。新版本里通知的相關(guān)功能被提取到了單獨(dú)的框架愈腾,通知也不再區(qū)分類型憋活,而有了更統(tǒng)一的行為。我們接下來(lái)就將由淺入深地解析這個(gè)重構(gòu)后的框架的使用方式虱黄。
UserNotifications 框架解析
基本流程
iOS 10 中通知相關(guān)的操作遵循下面的流程:
![](https://onevcat.com/assets/images/2016/notification-flow.png)
首先你需要向用戶請(qǐng)求推送權(quán)限悦即,然后發(fā)送通知。對(duì)于發(fā)送出的通知橱乱,如果你的應(yīng)用位于后臺(tái)或者沒(méi)有運(yùn)行的話辜梳,系統(tǒng)將通過(guò)用戶允許的方式 (彈窗,橫幅仅醇,或者是在通知中心) 進(jìn)行顯示冗美。如果你的應(yīng)用已經(jīng)位于前臺(tái)正在運(yùn)行,你可以自行決定要不要顯示這個(gè)通知析二。最后粉洼,如果你希望用戶點(diǎn)擊通知能有打開(kāi)應(yīng)用以外的額外功能的話,你也需要進(jìn)行處理叶摄。
權(quán)限申請(qǐng)
iOS 8 之前属韧,本地推送 UILocalNotification
和遠(yuǎn)程推送 Remote Notification
是區(qū)分對(duì)待的,應(yīng)用只需要在進(jìn)行遠(yuǎn)程推送時(shí)獲取用戶同意蛤吓。iOS 8 對(duì)這一行為進(jìn)行了規(guī)范宵喂,因?yàn)闊o(wú)論是本地推送還是遠(yuǎn)程推送,其實(shí)在用戶看來(lái)表現(xiàn)是一致的会傲,都是打斷用戶的行為锅棕。因此從 iOS 8 開(kāi)始拙泽,這兩種通知都需要申請(qǐng)權(quán)限。iOS 10 里進(jìn)一步消除了本地通知和推送通知的區(qū)別裸燎。向用戶申請(qǐng)通知權(quán)限非常簡(jiǎn)單:
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {
granted, error in
if granted {
// 用戶允許進(jìn)行通知
}
}
當(dāng)然顾瞻,在使用 UN 開(kāi)頭的 API 的時(shí)候,不要忘記導(dǎo)入 UserNotifications 框架:
import UserNotifications
第一次調(diào)用這個(gè)方法時(shí)德绿,會(huì)彈出一個(gè)系統(tǒng)彈窗荷荤。
![](https://onevcat.com/assets/images/2016/notification-auth-alert.png)
要注意的是,一旦用戶拒絕了這個(gè)請(qǐng)求移稳,再次調(diào)用該方法也不會(huì)再進(jìn)行彈窗蕴纳,想要應(yīng)用有機(jī)會(huì)接收到通知的話,用戶必須自行前往系統(tǒng)的設(shè)置中為你的應(yīng)用打開(kāi)通知个粱,如果不是殺手級(jí)應(yīng)用古毛,想讓用戶主動(dòng)去在茫茫多 app 中找到你的那個(gè)并專門為你開(kāi)啟通知,往往是不可能的几蜻。因此喇潘,在合適的時(shí)候彈出請(qǐng)求窗体斩,在請(qǐng)求權(quán)限前預(yù)先進(jìn)行說(shuō)明梭稚,以此增加通過(guò)的概率應(yīng)該是開(kāi)發(fā)者和策劃人員的必修課。相比與直接簡(jiǎn)單粗暴地在啟動(dòng)的時(shí)候就進(jìn)行彈窗絮吵,耐心誘導(dǎo)會(huì)是更明智的選擇弧烤。
一旦用戶同意后,你就可以在應(yīng)用中發(fā)送本地通知了蹬敲。不過(guò)如果你通過(guò)服務(wù)器發(fā)送遠(yuǎn)程通知的話暇昂,還需要多一個(gè)獲取用戶 token 的操作。你的服務(wù)器可以使用這個(gè) token 將用向 Apple Push Notification 的服務(wù)器提交請(qǐng)求伴嗡,然后 APNs 通過(guò) token 識(shí)別設(shè)備和應(yīng)用急波,將通知推給用戶。
提交 token 請(qǐng)求和獲得 token 的回調(diào)是現(xiàn)在“唯二”不在新框架中的 API瘪校。我們使用 UIApplication
的registerForRemoteNotifications
來(lái)注冊(cè)遠(yuǎn)程通知澄暮,在 AppDelegate
的 application(_:didRegisterForRemoteNotificationsWithDeviceToken)
中獲取用戶 token:
// 向 APNS 請(qǐng)求 token:
UIApplication.shared.registerForRemoteNotifications()
// AppDelegate.swift
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenString = deviceToken.hexString
print("Get Push token: \(tokenString)")
}
獲取得到的deviceToken
是一個(gè) Data
類型,為了方便使用和傳遞阱扬,我們一般會(huì)選擇將它轉(zhuǎn)換為一個(gè)字符串泣懊。Swift 3
中可以使用下面的 Data 擴(kuò)展來(lái)構(gòu)造出適合傳遞給 Apple 的字符串:
extension Data {
var hexString: String {
return withUnsafeBytes {(bytes: UnsafePointer<UInt8>) -> String in
let buffer = UnsafeBufferPointer(start: bytes, count: count)
return buffer.map {String(format: "%02hhx", $0)}.reduce("", { $0 + $1 })
}
}
}
權(quán)限設(shè)置
用戶可以在系統(tǒng)設(shè)置中修改你的應(yīng)用的通知權(quán)限,除了打開(kāi)和關(guān)閉全部通知權(quán)限外麻惶,用戶也可以限制你的應(yīng)用只能進(jìn)行某種形式的通知顯示馍刮,比如只允許橫幅而不允許彈窗及通知中心顯示等。一般來(lái)說(shuō)你不應(yīng)該對(duì)用戶的選擇進(jìn)行干涉窃蹋,但是如果你的應(yīng)用確實(shí)需要某種特定場(chǎng)景的推送的話卡啰,你可以對(duì)當(dāng)前用戶進(jìn)行的設(shè)置進(jìn)行檢查:
UNUserNotificationCenter.current().getNotificationSettings {
settings in
print(settings.authorizationStatus) // .authorized | .denied | .notDetermined
print(settings.badgeSetting) // .enabled | .disabled | .notSupported
// etc...
}
發(fā)送通知
UserNotifications
中對(duì)通知進(jìn)行了統(tǒng)一静稻。我們通過(guò)通知的內(nèi)容 (UNNotificationContent)
,發(fā)送的時(shí)機(jī)(UNNotificationTrigger)
以及一個(gè)發(fā)送通知的 String 類型的標(biāo)識(shí)符匈辱,來(lái)生成一個(gè) UNNotificationRequest
類型的發(fā)送請(qǐng)求姊扔。最后,我們將這個(gè)請(qǐng)求添加到 UNUserNotificationCenter.current()
中梅誓,就可以等待通知到達(dá)了:
// 1. 創(chuàng)建通知內(nèi)容
let content = UNMutableNotificationContent()
content.title = "Time Interval Notification"
content.body = "My first notification"
// 2. 創(chuàng)建發(fā)送觸發(fā)
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
// 3. 發(fā)送請(qǐng)求標(biāo)識(shí)符
let requestIdentifier = "com.onevcat.usernotification.myFirstNotification"
// 4. 創(chuàng)建一個(gè)發(fā)送請(qǐng)求
let request = UNNotificationRequest(identifier: requestIdentifier, content: content, trigger: trigger)
// 將請(qǐng)求添加到發(fā)送中心
UNUserNotificationCenter.current().add(request) { error in
if error == nil {
print("Time Interval Notification scheduled: \(requestIdentifier)")
}
}
iOS 10 中通知不僅支持簡(jiǎn)單的一行文字恰梢,你還可以添加 title
和 subtitle
,來(lái)用粗體字的形式強(qiáng)調(diào)通知的目的梗掰。對(duì)于遠(yuǎn)程推送嵌言,iOS 10 之前一般只含有消息的推送 payload 是這樣的:
{
"aps":{
"alert":"Test",
"sound":"default",
"badge":1
}
}
如果我們想要加入title
和 subtitle
的話,則需要將 alert
從字符串換為字典及穗,新的 payload 是:
{
"aps":{
"alert":{
"title":"I am title",
"subtitle":"I am subtitle",
"body":"I am body"
},
"sound":"default",
"badge":1
}
}
好消息是摧茴,后一種字典的方法其實(shí)在 iOS 8.2 的時(shí)候就已經(jīng)存在了。雖然當(dāng)時(shí)title
只是用在 Apple Watch
上的埂陆,但是設(shè)置好body
的話在 iOS 上還是可以顯示的苛白,所以針對(duì) iOS 10 添加標(biāo)題時(shí)是可以保證前向兼容的。
另外焚虱,如果要進(jìn)行本地化對(duì)應(yīng)购裙,在設(shè)置這些內(nèi)容文本時(shí),本地可以使用String.localizedUserNotificationString(forKey: "your_key", arguments: [])
的方式來(lái)從 Localizable.strings
文件中取出本地化字符串鹃栽,而遠(yuǎn)程推送的話躏率,也可以在payload
的 alert
中使用loc-key
或者title-loc-key
來(lái)進(jìn)行指定。關(guān)于 payload 中的 key民鼓,可以參考這篇文檔薇芝。
2.觸發(fā)器是只對(duì)本地通知而言的,遠(yuǎn)程推送的通知的話默認(rèn)會(huì)在收到后立即顯示》峒危現(xiàn)在UserNotifications
框架中提供了三種觸發(fā)器夯到,分別是:在一定時(shí)間后觸發(fā) UNTimeIntervalNotificationTrigger
,在某月某日某時(shí)觸發(fā)UNCalendarNotificationTrigger
以及在用戶進(jìn)入或是離開(kāi)某個(gè)區(qū)域時(shí)觸發(fā)UNLocationNotificationTrigger
饮亏。
3.請(qǐng)求標(biāo)識(shí)符可以用來(lái)區(qū)分不同的通知請(qǐng)求耍贾,在將一個(gè)通知請(qǐng)求提交后,通過(guò)特定 API 我們能夠使用這個(gè)標(biāo)識(shí)符來(lái)取消或者更新這個(gè)通知克滴。我們將在稍后再提到具體用法逼争。
4.在新版本的通知框架中,Apple 借用了一部分網(wǎng)絡(luò)請(qǐng)求的概念劝赔。我們組織并發(fā)送一個(gè)通知請(qǐng)求誓焦,然后將這個(gè)請(qǐng)求提交給 UNUserNotificationCenter
進(jìn)行處理。我們會(huì)在 delegate 中接收到這個(gè)通知請(qǐng)求對(duì)應(yīng)的 response
,另外我們也有機(jī)會(huì)在應(yīng)用的 extension
中對(duì)request
進(jìn)行處理杂伟。我們?cè)诮酉聛?lái)的章節(jié)會(huì)看到更多這方面的內(nèi)容移层。
在提交通知請(qǐng)求后,我們鎖屏或者將應(yīng)用切到后臺(tái)赫粥,并等待設(shè)定的時(shí)間后观话,就能看到我們的通知出現(xiàn)在通知中心或者屏幕橫幅了:
![](https://onevcat.com/assets/images/2016/notification-alert.png)
關(guān)于最基礎(chǔ)的通知發(fā)送,可以參考
Demo
中TimeIntervalViewController的內(nèi)容越平。
取消和更新
在創(chuàng)建通知請(qǐng)求時(shí)频蛔,我們已經(jīng)指定了標(biāo)識(shí)符。這個(gè)標(biāo)識(shí)符可以用來(lái)管理通知秦叛。在 iOS 10 之前,我們很難取消掉某一個(gè)特定的通知挣跋,也不能主動(dòng)移除或者更新已經(jīng)展示的通知。想象一下你需要推送用戶賬戶內(nèi)的余額變化情況避咆,多次的余額增減或者變化很容易讓用戶十分困惑 - 到底哪條通知才是最正確的舟肉?又或者在推送一場(chǎng)比賽的比分時(shí)路媚,頻繁的通知必然導(dǎo)致用戶通知中心數(shù)量爆炸,而大部分中途的比分對(duì)于用戶來(lái)說(shuō)只是噪音磷籍。
iOS 10 中适荣,UserNotifications 框架提供了一系列管理通知的 API,你可以做到:
- 取消還未展示的通知
- 更新還未展示的通知
- 移除已經(jīng)展示過(guò)的通知
- 更新已經(jīng)展示過(guò)的通知
其中關(guān)鍵就在于在創(chuàng)建請(qǐng)求時(shí)使用同樣的標(biāo)識(shí)符弛矛。
比如,從通知中心中移除一個(gè)展示過(guò)的通知:
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3, repeats: false)
let identifier = "com.onevcat.usernotification.notificationWillBeRemovedAfterDisplayed"
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if error != nil {
print("Notification request added: \(identifier)")
}
}
delay(4) {
print("Notification request removed: \(identifier)")
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [identifier])
}
類似地丈氓,我們可以使用 removePendingNotificationRequests
,來(lái)取消還未展示的通知請(qǐng)求万俗。對(duì)于更新通知,不論是否已經(jīng)展示闰歪,都和一開(kāi)始添加請(qǐng)求時(shí)一樣,再次將請(qǐng)求提交給 UNUserNotificationCenter
即可:
// let request: UNNotificationRequest = ...
UNUserNotificationCenter.current().add(request) { error in
if error != nil {
print("Notification request added: \(identifier)")
}
}
delay(2) {
let newTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
// Add new request with the same identifier to update a notification.
let newRequest = UNNotificationRequest(identifier: identifier, content:newContent, trigger: newTrigger)
UNUserNotificationCenter.current().add(newRequest) { error in
if error != nil {
print("Notification request updated: \(identifier)")
}
}
}
遠(yuǎn)程推送可以進(jìn)行通知的更新库倘,在使用 Provider API 向 APNs 提交請(qǐng)求時(shí)论矾,在 HTTP/2 的 header 中apns-collapse-id
key 的內(nèi)容將被作為該推送的標(biāo)識(shí)符進(jìn)行使用杆勇。多次推送同一標(biāo)識(shí)符的通知即可進(jìn)行更新。
對(duì)應(yīng)本地的removeDeliveredNotifications
蚜退,現(xiàn)在還不能通過(guò)類似的方式,向 APNs 發(fā)送一個(gè)包含 collapse id 的 DELETE 請(qǐng)求來(lái)刪除已經(jīng)展示的推送钻注,APNs 服務(wù)器并不接受一個(gè) DELETE 請(qǐng)求传黄。不過(guò)從技術(shù)上來(lái)說(shuō) Apple 方面應(yīng)該不存在什么問(wèn)題,我們可以拭目以待《涌埽現(xiàn)在如果想要消除一個(gè)遠(yuǎn)程推送膘掰,可以選擇使用后臺(tái)靜默推送的方式來(lái)從本地發(fā)起一個(gè)刪除通知的調(diào)用。關(guān)于后臺(tái)推送的部分佳遣,可以參考王巍之前的一篇關(guān)于 iOS7 中的多任務(wù)的文章识埋。
關(guān)于通知管理,可以參考 Demo 中 ManagementViewController
的內(nèi)容零渐。為了能夠簡(jiǎn)單地測(cè)試遠(yuǎn)程推送窒舟,一般我們都會(huì)用一些方便發(fā)送通知的工具,Knuff 就是其中之一诵盼。我也為 Knuff 添加了apns-collapse-id
的支持惠豺,你可以在這個(gè) fork 的 repo 或者是原 repo 的 pull request 中找到相關(guān)信息。