[轉載]iOS 10 UserNotifications 框架解析

活久見的重構 - iOS 10 UserNotifications 框架解析

TL;DR

iOS 10 中之前雜亂的和通知相關的 API 都被統一了,如今開發者可使用獨立的 UserNotifications.framework 來集中管理和使用 iOS 系統中通知的功能。在此基礎上,Apple 還增長了撤回單條通知,更新已展現通知,中途修改通知內容,在通知中展現圖片視頻,自定義通知 UI 等一系列新功能,很是強大。html

對於開發者來講,相較於以前版本,iOS 10 提供了一套很是易用通知處理接口,是 SDK 的一次重大重構。而以前的絕大部分通知相關 API 都已經被標爲棄用 (deprecated)。ios

這篇文章將首先回顧一下 Notification 的發展歷史和現狀,而後經過一些例子來展現 iOS 10 SDK 中相應的使用方式,來講明新 SDK 中通知能夠作的事情以及它們的使用方式。git

您能夠在 WWDC 16 的 Introduction to Notifications 和 Advanced Notifications 這兩個 Session 中找到詳細信息;另外也不要忘了參照UserNotifications 的官方文檔以及本文的實例項目 UserNotificationDemogithub

Notification 歷史和現狀

碎片化時間是移動設備用戶在使用應用時的一大特色,用戶但願隨時拿起手機就能查看資訊,處理事務,而通知能夠在重要的事件和信息發生時提醒用戶。完美的通知展現能夠很好地幫助用戶使用應用,體現出應用的價值,進而有很大可能將用戶帶回應用,提升活躍度。正因如此,不管是 Apple 仍是第三方開發者們,都很重視通知相關的開發工做,而通知也成爲了不少應用的必備功能,開發者們都但願通知能帶來更好地體驗和更多的用戶。web

可是理想的豐滿並不能彌補現實的骨感。自從在 iOS 3 引入 Push Notification 後,以後幾乎每一個版本 Apple 都在增強這方面的功能。咱們能夠回顧一下整個歷程和相關的主要 API:json

  • iOS 3 - 引入推送通知 UIApplication 的registerForRemoteNotificationTypes 與 UIApplicationDelegate 的application(_:didRegisterForRemoteNotificationsWithDeviceToken:)application(_:didReceiveRemoteNotification:)
  • iOS 4 - 引入本地通知scheduleLocalNotificationpresentLocalNotificationNow:application(_:didReceive:)
  • iOS 5 - 加入通知中心頁面
  • iOS 6 - 通知中心頁面與 iCloud 同步
  • iOS 7 - 後臺靜默推送application(_:didReceiveRemoteNotification:fetchCompletionHandle:)
  • iOS 8 - 從新設計 notification 權限請求,Actionable 通知registerUserNotificationSettings(_:)UIUserNotificationActionUIUserNotificationCategoryapplication(_:handleActionWithIdentifier:forRemoteNotification:completionHandler:) 等
  • iOS 9 - Text Input action,基於 HTTP/2 的推送請求UIUserNotificationActionBehavior,全新的 Provider API 等

有點暈,不是麼?一個開發者很難在不借助於文檔的幫助下區分application(_:didReceiveRemoteNotification:) 和application(_:didReceiveRemoteNotification:fetchCompletionHandle:),新入行的開發者也不可能明白 registerForRemoteNotificationTypes 和registerUserNotificationSettings(_:) 之間是否是有什麼關係,Remote 和 Local Notification 除了在初始化方式以外那些細微的區別也讓人抓狂,而不少 API 都被隨意地放在了 UIApplication 或者 UIApplicationDelegate中。除此以外,應用已經在前臺時,遠程推送是沒法直接顯示的,要先捕獲到遠程來的通知,而後再發起一個本地通知才能完成現實。更讓人鬱悶的是,應用在運行時和非運行時捕獲通知的路徑還不一致。雖然這些種種問題都是由必定歷史緣由形成的,但不能否認,正是混亂的組織方式和以前版本的考慮不周,使得 iOS 通知方面的開發一直稱不上「讓人愉悅」,甚至有很多「壞代碼」的味道。swift

另外一方面,如今的通知功能相對仍是簡單,咱們能作的只是本地或者遠程發起通知,而後顯示給用戶。雖然 iOS 8 和 9 中添加了按鈕和文原本進行交互,可是已發出的通知不能更新,通知的內容也只是在發起時惟一肯定,而這些內容也只能是簡單的文本。 想要在現有基礎上擴展通知的功能,勢必會讓本來就盤根錯節的 API 更加難以理解。api

在 iOS 10 中新加入 UserNotifications 框架,能夠說是 iOS SDK 發展到如今的最大規模的一次重構。新版本里通知的相關功能被提取到了單獨的框架,通知也再也不區分類型,而有了更統一的行爲。咱們接下來就將由淺入深地解析這個重構後的框架的使用方式。數組

UserNotifications 框架解析

基本流程

iOS 10 中通知相關的操做遵循下面的流程:緩存

首先你須要向用戶請求推送權限,而後發送通知。對於發送出的通知,若是你的應用位於後臺或者沒有運行的話,系統將經過用戶容許的方式 (彈窗,橫幅,或者是在通知中心) 進行顯示。若是你的應用已經位於前臺正在運行,你能夠自行決定要不要顯示這個通知。最後,若是你但願用戶點擊通知能有打開應用之外的額外功能的話,你也須要進行處理。

權限申請

通用權限

iOS 8 以前,本地推送 (UILocalNotification) 和遠程推送 (Remote Notification) 是區分對待的,應用只須要在進行遠程推送時獲取用戶贊成。iOS 8 對這一行爲進行了規範,由於不管是本地推送仍是遠程推送,其實在用戶看來表現是一致的,都是打斷用戶的行爲。所以從 iOS 8 開始,這兩種通知都須要申請權限。iOS 10 裏進一步消除了本地通知和推送通知的區別。向用戶申請通知權限很是簡單:

UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in if granted { // 用戶容許進行通知 } } 

固然,在使用 UN 開頭的 API 的時候,不要忘記導入 UserNotifications 框架:

import UserNotifications 

第一次調用這個方法時,會彈出一個系統彈窗。

要注意的是,一旦用戶拒絕了這個請求,再次調用該方法也不會再進行彈窗,想要應用有機會接收到通知的話,用戶必須自行前往系統的設置中爲你的應用打開通知,而這每每是不可能的。所以,在合適的時候彈出請求窗,在請求權限前預先進行說明,而不是直接粗暴地在啓動的時候就進行彈窗,會是更明智的選擇。

遠程推送

一旦用戶贊成後,你就能夠在應用中發送本地通知了。不過若是你經過服務器發送遠程通知的話,還須要多一個獲取用戶 token 的操做。你的服務器可使用這個 token 將用向 Apple Push Notification 的服務器提交請求,而後 APNs 經過 token 識別設備和應用,將通知推給用戶。

提交 token 請求和得到 token 的回調是如今「惟二」不在新框架中的 API。咱們使用 UIApplication 的 registerForRemoteNotifications 來註冊遠程通知,在 AppDelegate 的application(_:didRegisterForRemoteNotificationsWithDeviceToken) 中獲取用戶 token:

// 向 APNs 請求 token: UIApplication.shared.registerForRemoteNotifications() // AppDelegate.swift func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let tokenString = deviceToken.hexString print("Get Push token: \(tokenString)") } 

獲取獲得的 deviceToken 是一個 Data 類型,爲了方便使用和傳遞,咱們通常會選擇將它轉換爲一個字符串。Swift 3 中可使用下面的 Data 擴展來構造出適合傳遞給 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 }) } } } 

權限設置

用戶能夠在系統設置中修改你的應用的通知權限,除了打開和關閉所有通知權限外,用戶也能夠限制你的應用只能進行某種形式的通知顯示,好比只容許橫幅而不容許彈窗及通知中心顯示等。通常來講你不該該對用戶的選擇進行干涉,可是若是你的應用確實須要某種特定場景的推送的話,你能夠對當前用戶進行的設置進行檢查:

UNUserNotificationCenter.current().getNotificationSettings {
    settings in 
    print(settings.authorizationStatus) // .authorized | .denied | .notDetermined
    print(settings.badgeSetting) // .enabled | .disabled | .notSupported
    // etc...
}

關於權限方面的使用,能夠參考 Demo 中AuthorizationViewController 的內容。

發送通知

UserNotifications 中對通知進行了統一。咱們經過通知的內容 (UNNotificationContent),發送的時機 (UNNotificationTrigger) 以及一個發送通知的 String 類型的標識符,來生成一個 UNNotificationRequest 類型的發送請求。最後,咱們將這個請求添加到UNUserNotificationCenter.current() 中,就能夠等待通知到達了:

// 1. 建立通知內容 let content = UNMutableNotificationContent() content.title = "Time Interval Notification" content.body = "My first notification" // 2. 建立發送觸發 let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false) // 3. 發送請求標識符 let requestIdentifier = "com.onevcat.usernotification.myFirstNotification" // 4. 建立一個發送請求 let request = UNNotificationRequest(identifier: requestIdentifier, content: content, trigger: trigger) // 將請求添加到發送中心 UNUserNotificationCenter.current().add(request) { error in if error == nil { print("Time Interval Notification scheduled: \(requestIdentifier)") } } 
  1. iOS 10 中通知不只支持簡單的一行文字,你還能夠添加 title 和subtitle,來用粗體字的形式強調通知的目的。對於遠程推送,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 } } 

    好消息是,後一種字典的方法其實在 iOS 8.2 的時候就已經存在了。雖然當時 title 只是用在 Apple Watch 上的,可是設置好 body 的話在 iOS 上仍是能夠顯示的,因此針對 iOS 10 添加標題時是能夠保證前向兼容的。

    另外,若是要進行本地化對應,在設置這些內容文本時,本地可使用String.localizedUserNotificationString(forKey: "your_key", arguments: []) 的方式來從 Localizable.strings 文件中取出本地化字符串,而遠程推送的話,也能夠在 payload 的 alert 中使用 loc-key或者 title-loc-key 來進行指定。關於 payload 中的 key,能夠參考這篇文檔

  2. 觸發器是隻對本地通知而言的,遠程推送的通知的話默認會在收到後當即顯示。如今 UserNotifications 框架中提供了三種觸發器,分別是:在必定時間後觸發 UNTimeIntervalNotificationTrigger,在某月某日某時觸發 UNCalendarNotificationTrigger 以及在用戶進入或是離開某個區域時觸發 UNLocationNotificationTrigger

  3. 請求標識符能夠用來區分不一樣的通知請求,在將一個通知請求提交後,經過特定 API 咱們可以使用這個標識符來取消或者更新這個通知。咱們將在稍後再提到具體用法。

  4. 在新版本的通知框架中,Apple 借用了一部分網絡請求的概念。咱們組織併發送一個通知請求,而後將這個請求提交給UNUserNotificationCenter 進行處理。咱們會在 delegate 中接收到這個通知請求對應的 response,另外咱們也有機會在應用的 extension 中對 request 進行處理。咱們在接下來的章節會看到更多這方面的內容。

在提交通知請求後,咱們鎖屏或者將應用切到後臺,並等待設定的時間後,就能看到咱們的通知出如今通知中心或者屏幕橫幅了:

關於最基礎的通知發送,能夠參考 Demo 中TimeIntervalViewController 的內容。

取消和更新

在建立通知請求時,咱們已經指定了標識符。這個標識符能夠用來管理通知。在 iOS 10 以前,咱們很難取消掉某一個特定的通知,也不能主動移除或者更新已經展現的通知。想象一下你須要推送用戶帳戶內的餘額變化狀況,屢次的餘額增減或者變化很容易讓用戶十分困惑 - 到底哪條通知纔是最正確的?又或者在推送一場比賽的比分時,頻繁的通知必然致使用戶通知中心數量爆炸,而大部分中途的比分對於用戶來講只是噪音。

iOS 10 中,UserNotifications 框架提供了一系列管理通知的 API,你能夠作到:

  • 取消還未展現的通知
  • 更新還未展現的通知
  • 移除已經展現過的通知
  • 更新已經展現過的通知

其中關鍵就在於在建立請求時使用一樣的標識符。

好比,從通知中心中移除一個展現過的通知:

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,來取消還未展現的通知請求。對於更新通知,不管是否已經展現,都和一開始添加請求時同樣,再次將請求提交給 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)") } } } 

遠程推送能夠進行通知的更新,在使用 Provider API 向 APNs 提交請求時,在 HTTP/2 的 header 中 apns-collapse-id key 的內容將被做爲該推送的標識符進行使用。屢次推送同一標識符的通知便可進行更新。

對應本地的 removeDeliveredNotifications,如今還不能經過相似的方式,向 APNs 發送一個包含 collapse id 的 DELETE 請求來刪除已經展現的推送,APNs 服務器並不接受一個 DELETE 請求。不過從技術上來講 Apple 方面應該不存在什麼問題,咱們能夠拭目以待。如今若是想要消除一個遠程推送,能夠選擇使用後臺靜默推送的方式來從本地發起一個刪除通知的調用。關於後臺推送的部分,能夠參考我以前的一篇關於 iOS7 中的多任務的文章。

關於通知管理,能夠參考 Demo 中 ManagementViewController 的內容。爲了可以簡單地測試遠程推送,通常咱們都會用一些方便發送通知的工具,Knuff 就是其中之一。我也爲 Knuff 添加了 apns-collapse-id 的支持,你能夠在這個 fork 的 repo 或者是原 repo 的 pull request 中找到相關信息。

處理通知

應用內展現通知

如今系統能夠在應用處於後臺或者退出的時候向用戶展現通知了。不過,當應用處於前臺時,收到的通知是沒法進行展現的。若是咱們但願在應用內也能顯示通知的話,須要額外的工做。

UNUserNotificationCenterDelegate 提供了兩個方法,分別對應如何在應用內展現通知,和收到通知響應時要如何處理的工做。咱們能夠實現這個接口中的對應方法來在應用內展現通知:

class NotificationHandler: NSObject, UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.alert, .sound]) // 若是不想顯示某個通知,能夠直接用空 options 調用 completionHandler: // completionHandler([]) } } 

實現後,將 NotificationHandler 的實例賦值給UNUserNotificationCenter 的 delegate 屬性就能夠了。沒有特殊理由的話,AppDelegate 的 application(_:didFinishLaunchingWithOptions:) 就是一個不錯的選擇:

class AppDelegate: UIResponder, UIApplicationDelegate { let notificationHandler = NotificationHandler() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { UNUserNotificationCenter.current().delegate = notificationHandler return true } } 

對通知進行響應

UNUserNotificationCenterDelegate 中還有一個方法,userNotificationCenter(_:didReceive:withCompletionHandler:)。這個代理方法會在用戶與你推送的通知進行交互時被調用,包括用戶經過通知打開了你的應用,或者點擊或者觸發了某個 action (咱們以後會提到 actionable 的通知)。由於涉及到打開應用的行爲,因此實現了這個方法的 delegate 必須在 applicationDidFinishLaunching: 返回前就完成設置,這也是咱們以前推薦將 NotificationHandler 儘早進行賦值的理由。

一個最簡單的實現天然是什麼也不錯,直接告訴系統你已經完成了全部工做。

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { completionHandler() } 

想讓這個方法變得有趣一點的話,在建立通知的內容時,咱們能夠在請求中附帶一些信息:

let content = UNMutableNotificationContent() content.title = "Time Interval Notification" content.body = "My first notification" content.userInfo = ["name": "onevcat"] 

在該方法裏,咱們將獲取到這個推送請求對應的 response,UNNotificationResponse 是一個幾乎包括了通知的全部信息的對象,從中咱們能夠再次獲取到 userInfo 中的信息:

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { if let name = response.notification.request.content.userInfo["name"] as? String { print("I know it's you! \(name)") } completionHandler() } 

更好的消息是,遠程推送的 payload 內的內容也會出如今這個 userInfo中,這樣一來,不管是本地推送仍是遠程推送,處理的路徑獲得了統一。經過userInfo 的內容來決定頁面跳轉或者是進行其餘操做,都會有很大空間。

Actionable 通知發送和處理

註冊 Category

iOS 8 和 9 中 Apple 引入了能夠交互的通知,這是經過將一簇 action 放到一個 category 中,將這個 category 進行註冊,最後在發送通知時將通知的 category 設置爲要使用的 category 來實現的。

註冊一個 category 很是容易:

private func registerNotificationCategory() { let saySomethingCategory: UNNotificationCategory = { // 1 let inputAction = UNTextInputNotificationAction( identifier: "action.input", title: "Input", options: [.foreground], textInputButtonTitle: "Send", textInputPlaceholder: "What do you want to say...") // 2 let goodbyeAction = UNNotificationAction( identifier: "action.goodbye", title: "Goodbye", options: [.foreground]) let cancelAction = UNNotificationAction( identifier: "action.cancel", title: "Cancel", options: [.destructive]) // 3 return UNNotificationCategory(identifier:"saySomethingCategory", actions: [inputAction, goodbyeAction, cancelAction], intentIdentifiers: [], options: [.customDismissAction]) }() UNUserNotificationCenter.current().setNotificationCategories([saySomethingCategory]) } 
  1. UNTextInputNotificationAction 表明一個輸入文本的 action,你能夠自定義框的按鈕 title 和 placeholder。你稍後會使用 identifier 來對 action 進行區分。
  2. 普通的 UNNotificationAction 對應標準的按鈕。
  3. 爲 category 指定一個 identifier,咱們將在實際發送通知的時候用這個標識符進行設置,這樣系統就知道這個通知對應哪一個 category 了。

固然,不要忘了在程序啓動時調用這個方法進行註冊:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { registerNotificationCategory() UNUserNotificationCenter.current().delegate = notificationHandler return true } 

發送一個帶有 action 的通知

在完成 category 註冊後,發送一個 actionable 通知就很是簡單了,只須要在建立 UNNotificationContent 時把 categoryIdentifier 設置爲須要的 category id 便可:

content.categoryIdentifier = "saySomethingCategory" 

嘗試展現這個通知,在下拉或者使用 3D touch 展開通知後,就能夠看到對應的 action 了:

遠程推送也可使用 category,只須要在 payload 中添加 category 字段,並指定預先定義的 category id 就能夠了:

{
  "aps":{ "alert":"Please say something", "category":"saySomething" } } 

處理 actionable 通知

和普通的通知並沒有二致,actionable 通知也會走到 didReceive 的 delegate 方法,咱們經過 request 中包含的 categoryIdentifier 和 response 裏的actionIdentifier 就能夠輕易斷定是哪一個通知的哪一個操做被執行了。對於UNTextInputNotificationAction 觸發的 response,直接將它轉換爲一個UNTextInputNotificationResponse,就能夠拿到其中的用戶輸入了:

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> 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: String if let actionType = SaySomethingCategoryAction(rawValue: response.actionIdentifier) { switch actionType { case .input: text = (response as! UNTextInputNotificationResponse).userText case .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)") } } 

上面的代碼先判斷通知響應是否屬於 "saySomething",而後從用戶輸入或者是選擇中提取字符串,而且彈出一個 alert 做爲響應結果。固然,更多的狀況下咱們會發送一個網絡請求,或者是根據用戶操做更新一些 UI 等。

關於 Actionable 的通知,能夠參考 Demo 中ActionableViewController 的內容。

Notification Extension

iOS 10 中添加了不少 extension,做爲應用與系統整合的入口。與通知相關的 extension 有兩個:Service Extension 和 Content Extension。前者可讓咱們有機會在收到遠程推送的通知後,展現以前對通知內容進行修改;後者能夠用來自定義通知視圖的樣式。

截取並修改通知內容

NotificationService 的模板已經爲咱們進行了基本的實現:

class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? // 1 override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) if let bestAttemptContent = bestAttemptContent { if request.identifier == "mutableContent" { bestAttemptContent.body = "\(bestAttemptContent.body), onevcat" } contentHandler(bestAttemptContent) } } // 2 override 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) } } } 
  1. didReceive: 方法中有一個等待發送的通知請求,咱們經過修改這個請求中的 content 內容,而後在限制的時間內將修改後的內容調用經過contentHandler 返還給系統,就能夠顯示這個修改過的通知了。
  2. 在必定時間內沒有調用 contentHandler 的話,系統會調用這個方法,來告訴你大限已到。你能夠選擇什麼都不作,這樣的話系統將看成什麼都沒發生,簡單地顯示原來的通知。可能你其實已經設置好了絕大部份內容,只是有不多一部分沒有完成,這時你也能夠像例子中這樣調用contentHandler 來顯示一個變動「中途」的通知。

Service Extension 如今只對遠程推送的通知起效,你能夠在推送 payload 中增長一個 mutable-content 值爲 1 的項來啓用內容修改:

{
  "aps":{ "alert":{ "title":"Greetings", "body":"Long time no see" }, "mutable-content":1 } } 

這個 payload 的推送獲得的結果,注意 body 後面附上了名字。

使用在本機截取推送並替換內容的方式,能夠完成端到端 (end-to-end) 的推送加密。你在服務器推送 payload 中加入加密過的文本,在客戶端接到通知後使用預先定義或者獲取過的密鑰進行解密,而後當即顯示。這樣一來,即便推送信道被第三方截取,其中所傳遞的內容也仍是安全的。使用這種方式來發送密碼或者敏感信息,對於一些金融業務應用和聊天應用來講,應該是必備的特性。

在通知中展現圖片/視頻

相比於舊版本的通知,iOS 10 中另外一個亮眼功能是多媒體的推送。開發者如今能夠在通知中嵌入圖片或者視頻,這極大豐富了推送內容的可讀性和趣味性。

爲本地通知添加多媒體內容十分簡單,只須要經過本地磁盤上的文件 URL 建立一個 UNNotificationAttachment 對象,而後將這個對象放到數組中賦值給 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] } 

在顯示時,橫幅或者彈窗將附帶設置的圖片,使用 3D Touch pop 通知或者下拉通知顯示詳細內容時,圖片也會被放大展現:

除了圖片之外,通知還支持音頻以及視頻。你能夠將 MP3 或者 MP4 這樣的文件提供給系統來在通知中進行展現和播放。不過,這些文件都有尺寸的限制,好比圖片不能超過 5MB,視頻不能超過 50MB 等,不過對於通常的能在通知中展現的內容來講,這個尺寸應該是綽綽有餘了。關於支持的文件格式和尺寸,能夠在文檔中進行確認。在建立 UNNotificationAttachment 時,若是遇到了不支持的格式,SDK 也會拋出錯誤。

經過遠程推送的方式,你也能夠顯示圖片等多媒體內容。這要藉助於上一節所提到的經過 Notification Service Extension 來修改推送通知內容的技術。通常作法是,咱們在推送的 payload 中指定須要加載的圖片資源地址,這個地址能夠是應用 bundle 內已經存在的資源,也能夠是網絡的資源。不過由於在建立 UNNotificationAttachment 時咱們只能使用本地資源,因此若是多媒體還不在本地的話,咱們須要先將其下載到本地。在完成UNNotificationAttachment 建立後,咱們就能夠和本地通知同樣,將它設置給 attachments 屬性,而後調用 contentHandler 了。

簡單的示例 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 表示咱們會在接收到通知時對內容進行更改,image 指明瞭目標圖片的地址。

在 NotificationService 裏,加入以下代碼來下載圖片,並將其保存到磁盤緩存中:

private func downloadAndSave(url: URL, handler: @escaping (_ localURL: URL?) -> Void) { let task = URLSession.shared.dataTask(with: url, completionHandler: { data, res, error in var localURL: URL? = nil if let data = data { let ext = (url.absoluteString as NSString).pathExtension let 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) }) task.resume() } 

而後在 didReceive: 中,接收到這類通知時提取圖片地址,下載,並生成 attachment,進行通知展現:

if let imageURLString = bestAttemptContent.userInfo["image"] as? String, let URL = URL(string: imageURLString) { downloadAndSave(url: URL) { localURL in if let localURL = localURL { do { let attachment = try UNNotificationAttachment(identifier: "image_downloaded", url: localURL, options: nil) bestAttemptContent.attachments = [attachment] } catch { print(error) } } contentHandler(bestAttemptContent) } } 

關於在通知中展現圖片或者視頻,有幾點想補充說明:

  • UNNotificationContent 的 attachments 雖然是一個數組,可是系統只會展現第一個 attachment 對象的內容。不過你依然能夠發送多個 attachments,而後在要展現的時候再從新安排它們的順序,以顯示最符合情景的圖片或者視頻。另外,你也可能會在自定義通知展現 UI 時用到多個 attachment。咱們接下來一節中會看到一個相關的例子。
  • 在當前 beta (iOS 10 beta 4) 中,serviceExtensionTimeWillExpire被調用以前,你有 30 秒時間來處理和更改通知內容。對於通常的圖片來講,這個時間是足夠的。可是若是你推送的是體積較大的視頻內容,用戶又恰巧處在糟糕的網絡環境的話,頗有可能沒法及時下載完成。
  • 若是你想在遠程推送來的通知中顯示應用 bundle 內的資源的話,要注意 extension 的 bundle 和 app main bundle 並非一回事兒。你能夠選擇將圖片資源放到 extension bundle 中,也能夠選擇放在 main bundle 裏。總之,你須要保證可以獲取到正確的,而且你具備讀取權限的 url。關於從 extension 中訪問 main bundle,能夠參看這篇回答
  • 系統在建立 attachement 時會根據提供的 url 後綴肯定文件類型,若是沒有後綴,或者後綴沒法不正確的話,你能夠在建立時經過UNNotificationAttachmentOptionsTypeHintKey 來指定資源類型
  • 若是使用的圖片和視頻文件不在你的 bundle 內部,它們將被移動到系統的負責通知的文件夾下,而後在當通知被移除後刪除。若是媒體文件在 bundle 內部,它們將被複制到通知文件夾下。每一個應用能使用的媒體文件的文件大小總和是有限制,超過限制後建立 attachment 時將拋出異常。可能的全部錯誤能夠在 UNError 中找到。
  • 你能夠訪問一個已經建立的 attachment 的內容,可是要注意權限問題。可使用 startAccessingSecurityScopedResource 來暫時獲取以建立的 attachment 的訪問權限。好比:

    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() } } 

關於 Service Extension 和多媒體通知的使用,能夠參考 Demo 中NotificationService 和 MediaViewController 的內容。

自定義通知視圖樣式

iOS 10 SDK 新加的另外一個 Content Extension 能夠用來自定義通知的詳細頁面的視圖。新建一個 Notification Content Extension,Xcode 爲咱們準備的模板中包含了一個實現了 UNNotificationContentExtension 的UIViewController 子類。這個 extension 中有一個必須實現的方法didReceive(_:),在系統須要顯示自定義樣式的通知詳情視圖時,這個方法將被調用,你須要在其中配置你的 UI。而 UI 自己能夠經過這個 extension 中的 MainInterface.storyboard 來進行定義。自定義 UI 的通知是和通知 category 綁定的,咱們須要在 extension 的 Info.plist 裏指定這個通知樣式所對應的 category 標識符:

系統在接收到通知後會先查找有沒有可以處理這類通知的 content extension,若是存在,那麼就交給 extension 來進行處理。另外,在構建 UI 時,咱們能夠經過 Info.plist 控制通知詳細視圖的尺寸,以及是否顯示原始的通知。關於 Content Extension 中的 Info.plist 的 key,能夠在這個文檔中找到詳細信息。

雖然咱們可使用包括按鈕在內的各類 UI,可是系統不容許咱們對這些 UI 進行交互。點擊通知視圖 UI 自己會將咱們導航到應用中,不過咱們能夠經過 action 的方式來對自定義 UI 進行更新。UNNotificationContentExtension爲咱們提供了一個可選方法 didReceive(_:completionHandler:),它會在用戶選擇了某個 action 時被調用,你有機會在這裏更新通知的 UI。若是有 UI 更新,那麼在方法的 completionHandler 中,開發者能夠選擇傳遞.doNotDismiss 來保持通知繼續被顯示。若是沒有繼續顯示的必要,能夠選擇 .dismissAndForwardAction 或者 .dismiss,前者將把通知的 action 繼續傳遞給應用的 UNUserNotificationCenterDelegate 中的userNotificationCenter(:didReceive:withCompletionHandler),然後者將直接解散這個通知。

若是你的自定義 UI 包含視頻等,你還能夠實現UNNotificationContentExtension 裏的 media 開頭的一系列屬性,它將爲你提供一些視頻播放的控件和相關方法。

關於 Content Extension 和自定義通知樣式,能夠參考 Demo 中NotificationViewController 和 CustomizeUIViewController 的內容。

總結

iOS 10 SDK 中對通知這塊進行了 iOS 系統發佈以來最大的一次重構,不少「老朋友」都被標記爲了 deprecated:

iOS 10 中被標爲棄用的 API

  • UILocalNotification
  • UIMutableUserNotificationAction
  • UIMutableUserNotificationCategory
  • UIUserNotificationAction
  • UIUserNotificationCategory
  • UIUserNotificationSettings
  • handleActionWithIdentifier:forLocalNotification:
  • handleActionWithIdentifier:forRemoteNotification:
  • didReceiveLocalNotification:withCompletion:
  • didReceiveRemoteNotification:withCompletion:

等一系列在 UIKit 中的發送和處理通知的類型及方法。

現狀以及儘快使用新的 API

相比於 iOS 早期時代的 API,新的 API 展示出了高度的模塊化和統一特性,易用性也很是好,是一套更加先進的 API。若是有可能,特別是若是你的應用是重度依賴通知特性的話,直接從 iOS 10 開始可讓你充分使用在新通知體系的各類特性。

雖然原來的 API 都被標爲棄用了,可是若是你須要支持 iOS 10 以前的系統的話,你仍是須要使用原來的 API。咱們可使用

if #available(iOS 10.0, *) { // Use UserNotification } 

的方式來指針對 iOS 10 進行新通知的適配,並讓 iOS 10 的用戶享受到新通知帶來的便利特性,而後在未來版本升級到只支持 iOS 10 以上時再移除掉全部被棄用的代碼。對於優化和梳理通知相關代碼來講,新 API 對代碼設計和組織上帶來的好處足以彌補適配上的麻煩,並且它還能爲你的應用提供更好的通知特性和體驗,何樂不爲呢?

 

轉載至原文地址:https://onevcat.com/2016/08/notification/

相關文章
相關標籤/搜索