活久见的重构 - iOS 10 UserNotifications 框架解析
2019獨(dú)角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
TL;DR
iOS 10 中以前雜亂的和通知相關(guān)的 API 都被統(tǒng)一了,現(xiàn)在開發(fā)者可以使用獨(dú)立的 UserNotifications.framework 來集中管理和使用 iOS 系統(tǒng)中通知的功能。在此基礎(chǔ)上,Apple 還增加了撤回單條通知,更新已展示通知,中途修改通知內(nèi)容,在通知中展示圖片視頻,自定義通知 UI 等一系列新功能,非常強(qiáng)大。
對(duì)于開發(fā)者來說,相較于之前版本,iOS 10 提供了一套非常易用通知處理接口,是 SDK 的一次重大重構(gòu)。而之前的絕大部分通知相關(guān) API 都已經(jīng)被標(biāo)為棄用 (deprecated)。
這篇文章將首先回顧一下 Notification 的發(fā)展歷史和現(xiàn)狀,然后通過一些例子來展示 iOS 10 SDK 中相應(yīng)的使用方式,來說明新 SDK 中通知可以做的事情以及它們的使用方式。
您可以在 WWDC 16 的?Introduction to Notifications?和?Advanced Notifications?這兩個(gè) Session 中找到詳細(xì)信息;另外也不要忘了參照?UserNotifications 的官方文檔以及本文的實(shí)例項(xiàng)目 UserNotificationDemo。
Notification 歷史和現(xiàn)狀
碎片化時(shí)間是移動(dòng)設(shè)備用戶在使用應(yīng)用時(shí)的一大特點(diǎn),用戶希望隨時(shí)拿起手機(jī)就能查看資訊,處理事務(wù),而通知可以在重要的事件和信息發(fā)生時(shí)提醒用戶。完美的通知展示可以很好地幫助用戶使用應(yīng)用,體現(xiàn)出應(yīng)用的價(jià)值,進(jìn)而有很大可能將用戶帶回應(yīng)用,提高活躍度。正因如此,不論是 Apple 還是第三方開發(fā)者們,都很重視通知相關(guān)的開發(fā)工作,而通知也成為了很多應(yīng)用的必備功能,開發(fā)者們都希望通知能帶來更好地體驗(yàn)和更多的用戶。
但是理想的豐滿并不能彌補(bǔ)現(xiàn)實(shí)的骨感。自從在 iOS 3 引入 Push Notification 后,之后幾乎每個(gè)版本 Apple 都在加強(qiáng)這方面的功能。我們可以回顧一下整個(gè)歷程和相關(guān)的主要 API:
- iOS 3 - 引入推送通知?UIApplication?的?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 等
有點(diǎn)暈,不是么?一個(gè)開發(fā)者很難在不借助于文檔的幫助下區(qū)分?application(_:didReceiveRemoteNotification:)?和?application(_:didReceiveRemoteNotification:fetchCompletionHandle:),新入行的開發(fā)者也不可能明白?registerForRemoteNotificationTypes?和?registerUserNotificationSettings(_:)?之間是不是有什么關(guān)系,Remote 和 Local Notification 除了在初始化方式之外那些細(xì)微的區(qū)別也讓人抓狂,而很多 API 都被隨意地放在了?UIApplication?或者?UIApplicationDelegate?中。除此之外,應(yīng)用已經(jīng)在前臺(tái)時(shí),遠(yuǎn)程推送是無法直接顯示的,要先捕獲到遠(yuǎn)程來的通知,然后再發(fā)起一個(gè)本地通知才能完成現(xiàn)實(shí)。更讓人郁悶的是,應(yīng)用在運(yùn)行時(shí)和非運(yùn)行時(shí)捕獲通知的路徑還不一致。雖然這些種種問題都是由一定歷史原因造成的,但不可否認(rèn),正是混亂的組織方式和之前版本的考慮不周,使得 iOS 通知方面的開發(fā)一直稱不上“讓人愉悅”,甚至有不少“壞代碼”的味道。
另一方面,現(xiàn)在的通知功能相對(duì)還是簡(jiǎn)單,我們能做的只是本地或者遠(yuǎn)程發(fā)起通知,然后顯示給用戶。雖然 iOS 8 和 9 中添加了按鈕和文本來進(jìn)行交互,但是已發(fā)出的通知不能更新,通知的內(nèi)容也只是在發(fā)起時(shí)唯一確定,而這些內(nèi)容也只能是簡(jiǎn)單的文本。 想要在現(xiàn)有基礎(chǔ)上擴(kuò)展通知的功能,勢(shì)必會(huì)讓原本就盤根錯(cuò)節(jié)的 API 更加難以理解。
在 iOS 10 中新加入 UserNotifications 框架,可以說是 iOS SDK 發(fā)展到現(xiàn)在的最大規(guī)模的一次重構(gòu)。新版本里通知的相關(guān)功能被提取到了單獨(dú)的框架,通知也不再區(qū)分類型,而有了更統(tǒng)一的行為。我們接下來就將由淺入深地解析這個(gè)重構(gòu)后的框架的使用方式。
UserNotifications 框架解析
基本流程
iOS 10 中通知相關(guān)的操作遵循下面的流程:
首先你需要向用戶請(qǐng)求推送權(quán)限,然后發(fā)送通知。對(duì)于發(fā)送出的通知,如果你的應(yīng)用位于后臺(tái)或者沒有運(yùn)行的話,系統(tǒng)將通過用戶允許的方式 (彈窗,橫幅,或者是在通知中心) 進(jìn)行顯示。如果你的應(yīng)用已經(jīng)位于前臺(tái)正在運(yùn)行,你可以自行決定要不要顯示這個(gè)通知。最后,如果你希望用戶點(diǎn)擊通知能有打開應(yīng)用以外的額外功能的話,你也需要進(jìn)行處理。
權(quán)限申請(qǐng)
通用權(quán)限
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論是本地推送還是遠(yuǎn)程推送,其實(shí)在用戶看來表現(xiàn)是一致的,都是打斷用戶的行為。因此從 iOS 8 開始,這兩種通知都需要申請(qǐng)權(quán)限。iOS 10 里進(jìn)一步消除了本地通知和推送通知的區(qū)別。向用戶申請(qǐng)通知權(quán)限非常簡(jiǎn)單:
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {granted, error inif granted {// 用戶允許進(jìn)行通知} }當(dāng)然,在使用 UN 開頭的 API 的時(shí)候,不要忘記導(dǎo)入 UserNotifications 框架:
import UserNotifications第一次調(diào)用這個(gè)方法時(shí),會(huì)彈出一個(gè)系統(tǒng)彈窗。
要注意的是,一旦用戶拒絕了這個(gè)請(qǐng)求,再次調(diào)用該方法也不會(huì)再進(jìn)行彈窗,想要應(yīng)用有機(jī)會(huì)接收到通知的話,用戶必須自行前往系統(tǒng)的設(shè)置中為你的應(yīng)用打開通知,而這往往是不可能的。因此,在合適的時(shí)候彈出請(qǐng)求窗,在請(qǐng)求權(quán)限前預(yù)先進(jìn)行說明,而不是直接粗暴地在啟動(dòng)的時(shí)候就進(jìn)行彈窗,會(huì)是更明智的選擇。
遠(yuǎn)程推送
一旦用戶同意后,你就可以在應(yīng)用中發(fā)送本地通知了。不過如果你通過服務(wù)器發(fā)送遠(yuǎn)程通知的話,還需要多一個(gè)獲取用戶 token 的操作。你的服務(wù)器可以使用這個(gè) token 將用向 Apple Push Notification 的服務(wù)器提交請(qǐng)求,然后 APNs 通過 token 識(shí)別設(shè)備和應(yīng)用,將通知推給用戶。
提交 token 請(qǐng)求和獲得 token 的回調(diào)是現(xiàn)在“唯二”不在新框架中的 API。我們使用?UIApplication?的?registerForRemoteNotifications?來注冊(cè)遠(yuǎn)程通知,在?AppDelegate?的?application(_:didRegisterForRemoteNotificationsWithDeviceToken)?中獲取用戶 token:
// 向 APNs 請(qǐng)求 token: UIApplication.shared.registerForRemoteNotifications()// AppDelegate.swiftfunc application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {let tokenString = deviceToken.hexStringprint("Get Push token: \(tokenString)") }獲取得到的?deviceToken?是一個(gè)?Data?類型,為了方便使用和傳遞,我們一般會(huì)選擇將它轉(zhuǎn)換為一個(gè)字符串。Swift 3 中可以使用下面的?Data?擴(kuò)展來構(gòu)造出適合傳遞給 Apple 的字符串:
extension Data {var hexString: String {return withUnsafeBytes {(bytes: UnsafePointer<UInt8>) -> String inlet 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)限,除了打開和關(guān)閉全部通知權(quán)限外,用戶也可以限制你的應(yīng)用只能進(jìn)行某種形式的通知顯示,比如只允許橫幅而不允許彈窗及通知中心顯示等。一般來說你不應(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 | .notDeterminedprint(settings.badgeSetting) // .enabled | .disabled | .notSupported// etc... }關(guān)于權(quán)限方面的使用,可以參考 Demo 中?AuthorizationViewController?的內(nèi)容。
發(fā)送通知
UserNotifications 中對(duì)通知進(jìn)行了統(tǒng)一。我們通過通知的內(nèi)容 (UNNotificationContent),發(fā)送的時(shí)機(jī) (UNNotificationTrigger) 以及一個(gè)發(fā)送通知的?String?類型的標(biāo)識(shí)符,來生成一個(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 inif error == nil {print("Time Interval Notification scheduled: \(requestIdentifier)")} }iOS 10 中通知不僅支持簡(jiǎn)單的一行文字,你還可以添加?title?和?subtitle,來用粗體字的形式強(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: [])?的方式來從 Localizable.strings 文件中取出本地化字符串,而遠(yuǎn)程推送的話,也可以在 payload 的 alert 中使用?loc-key?或者?title-loc-key?來進(jìn)行指定。關(guān)于 payload 中的 key,可以參考這篇文檔。
觸發(fā)器是只對(duì)本地通知而言的,遠(yuǎn)程推送的通知的話默認(rèn)會(huì)在收到后立即顯示。現(xiàn)在 UserNotifications 框架中提供了三種觸發(fā)器,分別是:在一定時(shí)間后觸發(fā)?UNTimeIntervalNotificationTrigger,在某月某日某時(shí)觸發(fā)?UNCalendarNotificationTrigger?以及在用戶進(jìn)入或是離開某個(gè)區(qū)域時(shí)觸發(fā)?UNLocationNotificationTrigger。
請(qǐng)求標(biāo)識(shí)符可以用來區(qū)分不同的通知請(qǐng)求,在將一個(gè)通知請(qǐng)求提交后,通過特定 API 我們能夠使用這個(gè)標(biāo)識(shí)符來取消或者更新這個(gè)通知。我們將在稍后再提到具體用法。
在新版本的通知框架中,Apple 借用了一部分網(wǎng)絡(luò)請(qǐng)求的概念。我們組織并發(fā)送一個(gè)通知請(qǐng)求,然后將這個(gè)請(qǐng)求提交給?UNUserNotificationCenter?進(jìn)行處理。我們會(huì)在 delegaet 中接收到這個(gè)通知請(qǐng)求對(duì)應(yīng)的 response,另外我們也有機(jī)會(huì)在應(yīng)用的 extension 中對(duì) request 進(jìn)行處理。我們?cè)诮酉聛淼恼鹿?jié)會(huì)看到更多這方面的內(nèi)容。
在提交通知請(qǐng)求后,我們鎖屏或者將應(yīng)用切到后臺(tái),并等待設(shè)定的時(shí)間后,就能看到我們的通知出現(xiàn)在通知中心或者屏幕橫幅了:
關(guān)于最基礎(chǔ)的通知發(fā)送,可以參考 Demo 中?TimeIntervalViewController?的內(nèi)容。
取消和更新
在創(chuàng)建通知請(qǐng)求時(shí),我們已經(jīng)指定了標(biāo)識(shí)符。這個(gè)標(biāo)識(shí)符可以用來管理通知。在 iOS 10 之前,我們很難取消掉某一個(gè)特定的通知,也不能主動(dòng)移除或者更新已經(jīng)展示的通知。想象一下你需要推送用戶賬戶內(nèi)的余額變化情況,多次的余額增減或者變化很容易讓用戶十分困惑 - 到底哪條通知才是最正確的?又或者在推送一場(chǎng)比賽的比分時(shí),頻繁的通知必然導(dǎo)致用戶通知中心數(shù)量爆炸,而大部分中途的比分對(duì)于用戶來說只是噪音。
iOS 10 中,UserNotifications 框架提供了一系列管理通知的 API,你可以做到:
- 取消還未展示的通知
- 更新還未展示的通知
- 移除已經(jīng)展示過的通知
- 更新已經(jīng)展示過的通知
其中關(guān)鍵就在于在創(chuàng)建請(qǐng)求時(shí)使用同樣的標(biāo)識(shí)符。
比如,從通知中心中移除一個(gè)展示過的通知:
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 inif error != nil {print("Notification request added: \(identifier)")} }delay(4) {print("Notification request removed: \(identifier)")UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [identifier]) }類似地,我們可以使用?removePendingNotificationRequests,來取消還未展示的通知請(qǐng)求。對(duì)于更新通知,不論是否已經(jīng)展示,都和一開始添加請(qǐng)求時(shí)一樣,再次將請(qǐng)求提交給?UNUserNotificationCenter?即可:
// let request: UNNotificationRequest = ... UNUserNotificationCenter.current().add(request) { error inif 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 inif 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)在還不能通過類似的方式,向 APNs 發(fā)送一個(gè)包含 collapse id 的 DELETE 請(qǐng)求來刪除已經(jīng)展示的推送,APNs 服務(wù)器并不接受一個(gè) DELETE 請(qǐng)求。不過從技術(shù)上來說 Apple 方面應(yīng)該不存在什么問題,我們可以拭目以待。現(xiàn)在如果想要消除一個(gè)遠(yuǎn)程推送,可以選擇使用后臺(tá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)信息。
處理通知
應(yīng)用內(nèi)展示通知
現(xiàn)在系統(tǒng)可以在應(yīng)用處于后臺(tái)或者退出的時(shí)候向用戶展示通知了。不過,當(dāng)應(yīng)用處于前臺(tái)時(shí),收到的通知是無法進(jìn)行展示的。如果我們希望在應(yīng)用內(nèi)也能顯示通知的話,需要額外的工作。
UNUserNotificationCenterDelegate?提供了兩個(gè)方法,分別對(duì)應(yīng)如何在應(yīng)用內(nèi)展示通知,和收到通知響應(yīng)時(shí)要如何處理的工作。我們可以實(shí)現(xiàn)這個(gè)接口中的對(duì)應(yīng)方法來在應(yīng)用內(nèi)展示通知:
class NotificationHandler: NSObject, UNUserNotificationCenterDelegate {func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: (UNNotificationPresentationOptions) -> Void) {completionHandler([.alert, .sound])// 如果不想顯示某個(gè)通知,可以直接用空 options 調(diào)用 completionHandler:// completionHandler([])} }實(shí)現(xiàn)后,將?NotificationHandler?的實(shí)例賦值給?UNUserNotificationCenter?的?delegate?屬性就可以了。沒有特殊理由的話,AppDelegate 的?application(_:didFinishLaunchingWithOptions:)?就是一個(gè)不錯(cuò)的選擇:
class AppDelegate: UIResponder, UIApplicationDelegate {let notificationHandler = NotificationHandler()func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {UNUserNotificationCenter.current().delegate = notificationHandlerreturn true} }對(duì)通知進(jìn)行響應(yīng)
UNUserNotificationCenterDelegate?中還有一個(gè)方法,userNotificationCenter(_:didReceive:withCompletionHandler:)。這個(gè)代理方法會(huì)在用戶與你推送的通知進(jìn)行交互時(shí)被調(diào)用,包括用戶通過通知打開了你的應(yīng)用,或者點(diǎn)擊或者觸發(fā)了某個(gè) action (我們之后會(huì)提到 actionable 的通知)。因?yàn)樯婕暗酱蜷_應(yīng)用的行為,所以實(shí)現(xiàn)了這個(gè)方法的 delegate 必須在?applicationDidFinishLaunching:?返回前就完成設(shè)置,這也是我們之前推薦將?NotificationHandler?盡早進(jìn)行賦值的理由。
一個(gè)最簡(jiǎn)單的實(shí)現(xiàn)自然是什么也不錯(cuò),直接告訴系統(tǒng)你已經(jīng)完成了所有工作。
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: () -> Void) {completionHandler() }想讓這個(gè)方法變得有趣一點(diǎn)的話,在創(chuàng)建通知的內(nèi)容時(shí),我們可以在請(qǐng)求中附帶一些信息:
let content = UNMutableNotificationContent() content.title = "Time Interval Notification" content.body = "My first notification"content.userInfo = ["name": "onevcat"]在該方法里,我們將獲取到這個(gè)推送請(qǐng)求對(duì)應(yīng)的 response,UNNotificationResponse?是一個(gè)幾乎包括了通知的所有信息的對(duì)象,從中我們可以再次獲取到?userInfo?中的信息:
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: () -> Void) {if let name = response.notification.request.content.userInfo["name"] as? String {print("I know it's you! \(name)")}completionHandler() }更好的消息是,遠(yuǎn)程推送的 payload 內(nèi)的內(nèi)容也會(huì)出現(xiàn)在這個(gè)?userInfo?中,這樣一來,不論是本地推送還是遠(yuǎn)程推送,處理的路徑得到了統(tǒng)一。通過?userInfo?的內(nèi)容來決定頁(yè)面跳轉(zhuǎn)或者是進(jìn)行其他操作,都會(huì)有很大空間。
Actionable 通知發(fā)送和處理
注冊(cè) Category
iOS 8 和 9 中 Apple 引入了可以交互的通知,這是通過將一簇 action 放到一個(gè) category 中,將這個(gè) category 進(jìn)行注冊(cè),最后在發(fā)送通知時(shí)將通知的 category 設(shè)置為要使用的 category 來實(shí)現(xiàn)的。
注冊(cè)一個(gè) category 非常容易:
private func registerNotificationCategory() {let saySomethingCategory: UNNotificationCategory = {// 1let inputAction = UNTextInputNotificationAction(identifier: "action.input",title: "Input",options: [.foreground],textInputButtonTitle: "Send",textInputPlaceholder: "What do you want to say...")// 2let goodbyeAction = UNNotificationAction(identifier: "action.goodbye",title: "Goodbye",options: [.foreground])let cancelAction = UNNotificationAction(identifier: "action.cancel",title: "Cancel",options: [.destructive])// 3return UNNotificationCategory(identifier:"saySomethingCategory", actions: [inputAction, goodbyeAction, cancelAction], intentIdentifiers: [], options: [.customDismissAction])}()UNUserNotificationCenter.current().setNotificationCategories([saySomethingCategory]) }當(dāng)然,不要忘了在程序啟動(dòng)時(shí)調(diào)用這個(gè)方法進(jìn)行注冊(cè):
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {registerNotificationCategory()UNUserNotificationCenter.current().delegate = notificationHandlerreturn true }發(fā)送一個(gè)帶有 action 的通知
在完成 category 注冊(cè)后,發(fā)送一個(gè) actionable 通知就非常簡(jiǎn)單了,只需要在創(chuàng)建?UNNotificationContent?時(shí)把?categoryIdentifier?設(shè)置為需要的 category id 即可:
content.categoryIdentifier = "saySomethingCategory"嘗試展示這個(gè)通知,在下拉或者使用 3D touch 展開通知后,就可以看到對(duì)應(yīng)的 action 了:
遠(yuǎn)程推送也可以使用 category,只需要在 payload 中添加?category?字段,并指定預(yù)先定義的 category id 就可以了:
{"aps":{"alert":"Please say something","category":"saySomething"} }處理 actionable 通知
和普通的通知并無二致,actionable 通知也會(huì)走到?didReceive?的 delegate 方法,我們通過 request 中包含的?categoryIdentifier?和 response 里的?actionIdentifier?就可以輕易判定是哪個(gè)通知的哪個(gè)操作被執(zhí)行了。對(duì)于?UNTextInputNotificationAction?觸發(fā)的 response,直接將它轉(zhuǎn)換為一個(gè)?UNTextInputNotificationResponse,就可以拿到其中的用戶輸入了:
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: () -> Void) {if let category = UserNotificationCategoryType(rawValue: response.notification.request.content.categoryIdentifier) {switch category {case .saySomething:handleSaySomthing(response: response)}}completionHandler() }private func handleSaySomthing(response: UNNotificationResponse) {let text: Stringif let actionType = SaySomethingCategoryAction(rawValue: response.actionIdentifier) {switch actionType {case .input: text = (response as! UNTextInputNotificationResponse).userTextcase .goodbye: text = "Goodbye"case .none: text = ""}} else {// Only tap or clear. (You will not receive this callback when user clear your notification unless you set .customDismissAction as the option of category)text = ""}if !text.isEmpty {UIAlertController.showConfirmAlertFromTopViewController(message: "You just said \(text)")} }上面的代碼先判斷通知響應(yīng)是否屬于 "saySomething",然后從用戶輸入或者是選擇中提取字符串,并且彈出一個(gè) alert 作為響應(yīng)結(jié)果。當(dāng)然,更多的情況下我們會(huì)發(fā)送一個(gè)網(wǎng)絡(luò)請(qǐng)求,或者是根據(jù)用戶操作更新一些 UI 等。
關(guān)于 Actionable 的通知,可以參考 Demo 中?ActionableViewController?的內(nèi)容。
Notification Extension
iOS 10 中添加了很多 extension,作為應(yīng)用與系統(tǒng)整合的入口。與通知相關(guān)的 extension 有兩個(gè):Service Extension 和 Content Extension。前者可以讓我們有機(jī)會(huì)在收到遠(yuǎn)程推送的通知后,展示之前對(duì)通知內(nèi)容進(jìn)行修改;后者可以用來自定義通知視圖的樣式。
截取并修改通知內(nèi)容
NotificationService?的模板已經(jīng)為我們進(jìn)行了基本的實(shí)現(xiàn):
class NotificationService: UNNotificationServiceExtension {var contentHandler: ((UNNotificationContent) -> Void)?var bestAttemptContent: UNMutableNotificationContent?// 1override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler:(UNNotificationContent) -> Void) {self.contentHandler = contentHandlerbestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)if let bestAttemptContent = bestAttemptContent {if request.identifier == "mutableContent" {bestAttemptContent.body = "\(bestAttemptContent.body), onevcat"}contentHandler(bestAttemptContent)}}// 2override func serviceExtensionTimeWillExpire() {// Called just before the extension will be terminated by the system.// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {contentHandler(bestAttemptContent)}} }Service Extension 現(xiàn)在只對(duì)遠(yuǎn)程推送的通知起效,你可以在推送 payload 中增加一個(gè)?mutable-content?值為 1 的項(xiàng)來啟用內(nèi)容修改:
{"aps":{"alert":{"title":"Greetings","body":"Long time no see"},"mutable-content":1} }這個(gè) payload 的推送得到的結(jié)果,注意 body 后面附上了名字。
使用在本機(jī)截取推送并替換內(nèi)容的方式,可以完成端到端 (end-to-end) 的推送加密。你在服務(wù)器推送 payload 中加入加密過的文本,在客戶端接到通知后使用預(yù)先定義或者獲取過的密鑰進(jìn)行解密,然后立即顯示。這樣一來,即使推送信道被第三方截取,其中所傳遞的內(nèi)容也還是安全的。使用這種方式來發(fā)送密碼或者敏感信息,對(duì)于一些金融業(yè)務(wù)應(yīng)用和聊天應(yīng)用來說,應(yīng)該是必備的特性。
在通知中展示圖片/視頻
相比于就版本的通知,iOS 10 中另一個(gè)亮眼功能是多媒體的推送。開發(fā)者現(xiàn)在可以在通知中嵌入圖片或者視頻,這極大豐富了推送內(nèi)容的可讀性和趣味性。
為本地通知添加多媒體內(nèi)容十分簡(jiǎn)單,只需要通過本地磁盤上的文件 URL 創(chuàng)建一個(gè)?UNNotificationAttachment?對(duì)象,然后將這個(gè)對(duì)象放到數(shù)組中賦值給 content 的?attachments?屬性就行了:
let content = UNMutableNotificationContent() content.title = "Image Notification" content.body = "Show me an image!"if let imageURL = Bundle.main.url(forResource: "image", withExtension: "jpg"),let attachment = try? UNNotificationAttachment(identifier: "imageAttachment", url: imageURL, options: nil) {content.attachments = [attachment] }在顯示時(shí),橫幅或者彈窗將附帶設(shè)置的圖片,使用 3D Touch pop 通知或者下拉通知顯示詳細(xì)內(nèi)容時(shí),圖片也會(huì)被放大展示:
除了圖片以外,通知還支持音頻以及視頻。你可以將 MP3 或者 MP4 這樣的文件提供給系統(tǒng)來在通知中進(jìn)行展示和播放。不過,這些文件都有尺寸的限制,比如圖片不能超過 5MB,視頻不能超過 50MB 等,不過對(duì)于一般的能在通知中展示的內(nèi)容來說,這個(gè)尺寸應(yīng)該是綽綽有余了。關(guān)于支持的文件格式和尺寸,可以在文檔中進(jìn)行確認(rèn)。在創(chuàng)建?UNNotificationAttachment?時(shí),如果遇到了不支持的格式,SDK 也會(huì)拋出錯(cuò)誤。
通過遠(yuǎn)程推送的方式,你也可以顯示圖片等多媒體內(nèi)容。這要借助于上一節(jié)所提到的通過 Notification Service Extension 來修改推送通知內(nèi)容的技術(shù)。一般做法是,我們?cè)谕扑偷?payload 中指定需要加載的圖片資源地址,這個(gè)地址可以是應(yīng)用 bundle 內(nèi)已經(jīng)存在的資源,也可以是網(wǎng)絡(luò)的資源。不過因?yàn)樵趧?chuàng)建?UNNotificationAttachment?時(shí)我們只能使用本地資源,所以如果多媒體還不在本地的話,我們需要先將其下載到本地。在完成?UNNotificationAttachment?創(chuàng)建后,我們就可以和本地通知一樣,將它設(shè)置給?attachments?屬性,然后調(diào)用?contentHandler?了。
簡(jiǎn)單的示例 payload 如下:
{"aps":{"alert":{"title":"Image Notification","body":"Show me an image from web!"},"mutable-content":1},"image": "https://onevcat.com/assets/images/background-cover.jpg" }mutable-content?表示我們會(huì)在接收到通知時(shí)對(duì)內(nèi)容進(jìn)行更改,image?指明了目標(biāo)圖片的地址。
在?NotificationService?里,加入如下代碼來下載圖片,并將其保存到磁盤緩存中:
private func downloadAndSave(url: URL, handler: (localURL: URL?) -> Void) {let task = URLSession.shared.dataTask(with: url, completionHandler: {data, res, error invar localURL: URL? = nilif let data = data {let ext = (url.absoluteString as NSString).pathExtensionlet cacheURL = URL(fileURLWithPath: FileManager.default.cachesDirectory)let url = cacheURL.appendingPathComponent(url.absoluteString.md5).appendingPathExtension(ext)if let _ = try? data.write(to: url) {localURL = url}}handler(localURL: localURL)})task.resume() }然后在?didReceive:?中,接收到這類通知時(shí)提取圖片地址,下載,并生成 attachment,進(jìn)行通知展示:
if let imageURLString = bestAttemptContent.userInfo["image"] as? String,let URL = URL(string: imageURLString) {downloadAndSave(url: URL) { localURL inif let localURL = localURL {do {let attachment = try UNNotificationAttachment(identifier: "image_downloaded", url: localURL, options: nil)bestAttemptContent.attachments = [attachment]} catch {print(error)}}contentHandler(bestAttemptContent)} }關(guān)于在通知中展示圖片或者視頻,有幾點(diǎn)想補(bǔ)充說明:
- UNNotificationContent?的?attachments?雖然是一個(gè)數(shù)組,但是系統(tǒng)只會(huì)展示第一個(gè) attachment 對(duì)象的內(nèi)容。不過你依然可以發(fā)送多個(gè) attachments,然后在要展示的時(shí)候再重新安排它們的順序,以顯示最符合情景的圖片或者視頻。另外,你也可能會(huì)在自定義通知展示 UI 時(shí)用到多個(gè) attachment。我們接下來一節(jié)中會(huì)看到一個(gè)相關(guān)的例子。
- 在當(dāng)前 beta (iOS 10 beta 4) 中,serviceExtensionTimeWillExpire?被調(diào)用之前,你有 30 秒時(shí)間來處理和更改通知內(nèi)容。對(duì)于一般的圖片來說,這個(gè)時(shí)間是足夠的。但是如果你推送的是體積較大的視頻內(nèi)容,用戶又恰巧處在糟糕的網(wǎng)絡(luò)環(huán)境的話,很有可能無法及時(shí)下載完成。
- 如果你想在遠(yuǎn)程推送來的通知中顯示應(yīng)用 bundle 內(nèi)的資源的話,要注意 extension 的 bundle 和 app main bundle 并不是一回事兒。你可以選擇將圖片資源放到 extension bundle 中,也可以選擇放在 main bundle 里。總之,你需要保證能夠獲取到正確的,并且你具有讀取權(quán)限的 url。關(guān)于從 extension 中訪問 main bundle,可以參看這篇回答。
- 系統(tǒng)在創(chuàng)建 attachement 時(shí)會(huì)根據(jù)提供的 url 后綴確定文件類型,如果沒有后綴,或者后綴無法不正確的話,你可以在創(chuàng)建時(shí)通過?UNNotificationAttachmentOptionsTypeHintKey?來指定資源類型。
- 如果使用的圖片和視頻文件不在你的 bundle 內(nèi)部,它們將被移動(dòng)到系統(tǒng)的負(fù)責(zé)通知的文件夾下,然后在當(dāng)通知被移除后刪除。如果媒體文件在 bundle 內(nèi)部,它們將被復(fù)制到通知文件夾下。每個(gè)應(yīng)用能使用的媒體文件的文件大小總和是有限制,超過限制后創(chuàng)建 attachment 時(shí)將拋出異常。可能的所有錯(cuò)誤可以在?UNError?中找到。
-
你可以訪問一個(gè)已經(jīng)創(chuàng)建的 attachment 的內(nèi)容,但是要注意權(quán)限問題。可以使用?startAccessingSecurityScopedResource?來暫時(shí)獲取以創(chuàng)建的 attachment 的訪問權(quán)限。比如:
let content = notification.request.content if let attachment = content.attachments.first { if attachment.url.startAccessingSecurityScopedResource() { eventImage.image = UIImage(contentsOfFile: attachment.url.path!) attachment.url.stopAccessingSecurityScopedResource() } }
關(guān)于 Service Extension 和多媒體通知的使用,可以參考 Demo 中?NotificationService?和?MediaViewController?的內(nèi)容。
自定義通知視圖樣式
iOS 10 SDK 新加的另一個(gè) Content Extension 可以用來自定義通知的詳細(xì)頁(yè)面的視圖。新建一個(gè) Notification Content Extension,Xcode 為我們準(zhǔn)備的模板中包含了一個(gè)實(shí)現(xiàn)了?UNNotificationContentExtension?的?UIViewController?子類。這個(gè) extension 中有一個(gè)必須實(shí)現(xiàn)的方法?didReceive(_:),在系統(tǒng)需要顯示自定義樣式的通知詳情視圖時(shí),這個(gè)方法將被調(diào)用,你需要在其中配置你的 UI。而 UI 本身可以通過這個(gè) extension 中的 MainInterface.storyboard 來進(jìn)行定義。自定義 UI 的通知是和通知 category 綁定的,我們需要在 extension 的 Info.plist 里指定這個(gè)通知樣式所對(duì)應(yīng)的 category 標(biāo)識(shí)符:
系統(tǒng)在接收到通知后會(huì)先查找有沒有能夠處理這類通知的 content extension,如果存在,那么就交給 extension 來進(jìn)行處理。另外,在構(gòu)建 UI 時(shí),我們可以通過 Info.plist 控制通知詳細(xì)視圖的尺寸,以及是否顯示原始的通知。關(guān)于 Content Extension 中的 Info.plist 的 key,可以在這個(gè)文檔中找到詳細(xì)信息。
雖然我們可以使用包括按鈕在內(nèi)的各種 UI,但是系統(tǒng)不允許我們對(duì)這些 UI 進(jìn)行交互。點(diǎn)擊通知視圖 UI 本身會(huì)將我們導(dǎo)航到應(yīng)用中,不過我們可以通過 action 的方式來對(duì)自定義 UI 進(jìn)行更新。UNNotificationContentExtension?為我們提供了一個(gè)可選方法?didReceive(_:completionHandler:),它會(huì)在用戶選擇了某個(gè) action 時(shí)被調(diào)用,你有機(jī)會(huì)在這里更新通知的 UI。如果有 UI 更新,那么在方法的?completionHandler?中,開發(fā)者可以選擇傳遞?.doNotDismiss?來保持通知繼續(xù)被顯示。如果沒有繼續(xù)顯示的必要,可以選擇?.dismissAndForwardAction?或者?.dismiss,前者將把通知的 action 繼續(xù)傳遞給應(yīng)用的?UNUserNotificationCenterDelegate?中的?userNotificationCenter(:didReceive:withCompletionHandler),而后者將直接解散這個(gè)通知。
如果你的自定義 UI 包含視頻等,你還可以實(shí)現(xiàn)?UNNotificationContentExtension?里的?media?開頭的一系列屬性,它將為你提供一些視頻播放的控件和相關(guān)方法。
關(guān)于 Content Extension 和自定義通知樣式,可以參考 Demo 中?NotificationViewController?和?CustomizeUIViewController?的內(nèi)容。
總結(jié)
iOS 10 SDK 中對(duì)通知這塊進(jìn)行了 iOS 系統(tǒng)發(fā)布以來最大的一次重構(gòu),很多“老朋友”都被標(biāo)記為了 deprecated:
iOS 10 中被標(biāo)為棄用的 API
- UILocalNotification
- UIMutableUserNotificationAction
- UIMutableUserNotificationCategory
- UIUserNotificationAction
- UIUserNotificationCategory
- UIUserNotificationSettings
- handleActionWithIdentifier:forLocalNotification:
- handleActionWithIdentifier:forRemoteNotification:
- didReceiveLocalNotification:withCompletion:
- didReceiveRemoteNotification:withCompletion:
等一系列在?UIKit?中的發(fā)送和處理通知的類型及方法。
現(xiàn)狀以及盡快使用新的 API
相比于 iOS 早期時(shí)代的 API,新的 API 展現(xiàn)出了高度的模塊化和統(tǒng)一特性,易用性也非常好,是一套更加先進(jìn)的 API。如果有可能,特別是如果你的應(yīng)用是重度依賴通知特性的話,直接從 iOS 10 開始可以讓你充分使用在新通知體系的各種特性。
雖然原來的 API 都被標(biāo)為棄用了,但是如果你需要支持 iOS 10 之前的系統(tǒng)的話,你還是需要使用原來的 API。我們可以使用
if #available(iOS 10.0, *) {// Use UserNotification }的方式來指針對(duì) iOS 10 進(jìn)行新通知的適配,并讓 iOS 10 的用戶享受到新通知帶來的便利特性,然后在將來版本升級(jí)到只支持 iOS 10 以上時(shí)再移除掉所有被棄用的代碼。對(duì)于優(yōu)化和梳理通知相關(guān)代碼來說,新 API 對(duì)代碼設(shè)計(jì)和組織上帶來的好處足以彌補(bǔ)適配上的麻煩,而且它還能為你的應(yīng)用提供更好的通知特性和體驗(yàn),何樂不為呢?
轉(zhuǎn)載于:https://my.oschina.net/JiangTun/blog/729987
總結(jié)
以上是生活随笔為你收集整理的活久见的重构 - iOS 10 UserNotifications 框架解析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: centos6下时间同步(ntp)操作
- 下一篇: nginx启动报错