活久见的重构 - iOS 10 UserNotifications 框架解析
2019獨角獸企業重金招聘Python工程師標準>>>
TL;DR
iOS 10 中以前雜亂的和通知相關的 API 都被統一了,現在開發者可以使用獨立的 UserNotifications.framework 來集中管理和使用 iOS 系統中通知的功能。在此基礎上,Apple 還增加了撤回單條通知,更新已展示通知,中途修改通知內容,在通知中展示圖片視頻,自定義通知 UI 等一系列新功能,非常強大。
對于開發者來說,相較于之前版本,iOS 10 提供了一套非常易用通知處理接口,是 SDK 的一次重大重構。而之前的絕大部分通知相關 API 都已經被標為棄用 (deprecated)。
這篇文章將首先回顧一下 Notification 的發展歷史和現狀,然后通過一些例子來展示 iOS 10 SDK 中相應的使用方式,來說明新 SDK 中通知可以做的事情以及它們的使用方式。
您可以在 WWDC 16 的?Introduction to Notifications?和?Advanced Notifications?這兩個 Session 中找到詳細信息;另外也不要忘了參照?UserNotifications 的官方文檔以及本文的實例項目 UserNotificationDemo。
Notification 歷史和現狀
碎片化時間是移動設備用戶在使用應用時的一大特點,用戶希望隨時拿起手機就能查看資訊,處理事務,而通知可以在重要的事件和信息發生時提醒用戶。完美的通知展示可以很好地幫助用戶使用應用,體現出應用的價值,進而有很大可能將用戶帶回應用,提高活躍度。正因如此,不論是 Apple 還是第三方開發者們,都很重視通知相關的開發工作,而通知也成為了很多應用的必備功能,開發者們都希望通知能帶來更好地體驗和更多的用戶。
但是理想的豐滿并不能彌補現實的骨感。自從在 iOS 3 引入 Push Notification 后,之后幾乎每個版本 Apple 都在加強這方面的功能。我們可以回顧一下整個歷程和相關的主要 API:
- iOS 3 - 引入推送通知?UIApplication?的?registerForRemoteNotificationTypes?與?UIApplicationDelegate?的?application(_:didRegisterForRemoteNotificationsWithDeviceToken:),application(_:didReceiveRemoteNotification:)
- iOS 4 - 引入本地通知?scheduleLocalNotification,presentLocalNotificationNow:,?application(_:didReceive:)
- iOS 5 - 加入通知中心頁面
- iOS 6 - 通知中心頁面與 iCloud 同步
- iOS 7 - 后臺靜默推送?application(_:didReceiveRemoteNotification:fetchCompletionHandle:)
- iOS 8 - 重新設計 notification 權限請求,Actionable 通知?registerUserNotificationSettings(_:),UIUserNotificationAction?與?UIUserNotificationCategory,application(_: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 通知方面的開發一直稱不上“讓人愉悅”,甚至有不少“壞代碼”的味道。
另一方面,現在的通知功能相對還是簡單,我們能做的只是本地或者遠程發起通知,然后顯示給用戶。雖然 iOS 8 和 9 中添加了按鈕和文本來進行交互,但是已發出的通知不能更新,通知的內容也只是在發起時唯一確定,而這些內容也只能是簡單的文本。 想要在現有基礎上擴展通知的功能,勢必會讓原本就盤根錯節的 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 inif 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.swiftfunc application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {let tokenString = deviceToken.hexStringprint("Get Push token: \(tokenString)") }獲取得到的?deviceToken?是一個?Data?類型,為了方便使用和傳遞,我們一般會選擇將它轉換為一個字符串。Swift 3 中可以使用下面的?Data?擴展來構造出適合傳遞給 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 })}} }權限設置
用戶可以在系統設置中修改你的應用的通知權限,除了打開和關閉全部通知權限外,用戶也可以限制你的應用只能進行某種形式的通知顯示,比如只允許橫幅而不允許彈窗及通知中心顯示等。一般來說你不應該對用戶的選擇進行干涉,但是如果你的應用確實需要某種特定場景的推送的話,你可以對當前用戶進行的設置進行檢查:
UNUserNotificationCenter.current().getNotificationSettings {settings in print(settings.authorizationStatus) // .authorized | .denied | .notDeterminedprint(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 inif error == nil {print("Time Interval Notification scheduled: \(requestIdentifier)")} }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,可以參考這篇文檔。
觸發器是只對本地通知而言的,遠程推送的通知的話默認會在收到后立即顯示。現在 UserNotifications 框架中提供了三種觸發器,分別是:在一定時間后觸發?UNTimeIntervalNotificationTrigger,在某月某日某時觸發?UNCalendarNotificationTrigger?以及在用戶進入或是離開某個區域時觸發?UNLocationNotificationTrigger。
請求標識符可以用來區分不同的通知請求,在將一個通知請求提交后,通過特定 API 我們能夠使用這個標識符來取消或者更新這個通知。我們將在稍后再提到具體用法。
在新版本的通知框架中,Apple 借用了一部分網絡請求的概念。我們組織并發送一個通知請求,然后將這個請求提交給?UNUserNotificationCenter?進行處理。我們會在 delegaet 中接收到這個通知請求對應的 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 inif 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 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)")}} }遠程推送可以進行通知的更新,在使用 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: (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 = notificationHandlerreturn true} }對通知進行響應
UNUserNotificationCenterDelegate?中還有一個方法,userNotificationCenter(_:didReceive:withCompletionHandler:)。這個代理方法會在用戶與你推送的通知進行交互時被調用,包括用戶通過通知打開了你的應用,或者點擊或者觸發了某個 action (我們之后會提到 actionable 的通知)。因為涉及到打開應用的行為,所以實現了這個方法的 delegate 必須在?applicationDidFinishLaunching:?返回前就完成設置,這也是我們之前推薦將?NotificationHandler?盡早進行賦值的理由。
一個最簡單的實現自然是什么也不錯,直接告訴系統你已經完成了所有工作。
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: () -> 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: () -> 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 = {// 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]) }當然,不要忘了在程序啟動時調用這個方法進行注冊:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {registerNotificationCategory()UNUserNotificationCenter.current().delegate = notificationHandlerreturn 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: () -> 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)")} }上面的代碼先判斷通知響應是否屬于 "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?// 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 現在只對遠程推送的通知起效,你可以在推送 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: (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:?中,接收到這類通知時提取圖片地址,下載,并生成 attachment,進行通知展示:
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)} }關于在通知中展示圖片或者視頻,有幾點想補充說明:
- 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://my.oschina.net/JiangTun/blog/729987
總結
以上是生活随笔為你收集整理的活久见的重构 - iOS 10 UserNotifications 框架解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: centos6下时间同步(ntp)操作
- 下一篇: nginx启动报错